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.
Files changed (169) hide show
  1. {specsmith-0.4.0.dev221/src/specsmith.egg-info → specsmith-0.4.0.dev223}/PKG-INFO +1 -1
  2. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/pyproject.toml +9 -6
  3. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/agent/broker.py +6 -5
  4. specsmith-0.4.0.dev223/src/specsmith/agent/events.py +176 -0
  5. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/agent/indexer.py +1 -1
  6. specsmith-0.4.0.dev223/src/specsmith/agent/mcp.py +117 -0
  7. specsmith-0.4.0.dev223/src/specsmith/agent/memory.py +81 -0
  8. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/agent/orchestrator.py +24 -8
  9. specsmith-0.4.0.dev223/src/specsmith/agent/router.py +78 -0
  10. specsmith-0.4.0.dev223/src/specsmith/agent/rules.py +62 -0
  11. specsmith-0.4.0.dev223/src/specsmith/agent/verifier.py +123 -0
  12. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/cli.py +476 -3
  13. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/console_utils.py +4 -3
  14. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223/src/specsmith.egg-info}/PKG-INFO +1 -1
  15. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith.egg-info/SOURCES.txt +8 -0
  16. specsmith-0.4.0.dev223/tests/test_e2e_nexus.py +144 -0
  17. specsmith-0.4.0.dev223/tests/test_phase1_4_new.py +127 -0
  18. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/LICENSE +0 -0
  19. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/README.md +0 -0
  20. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/setup.cfg +0 -0
  21. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/epistemic/__init__.py +0 -0
  22. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/epistemic/belief.py +0 -0
  23. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/epistemic/certainty.py +0 -0
  24. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/epistemic/failure_graph.py +0 -0
  25. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/epistemic/py.typed +0 -0
  26. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/epistemic/recovery.py +0 -0
  27. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/epistemic/session.py +0 -0
  28. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/epistemic/stress_tester.py +0 -0
  29. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/epistemic/trace.py +0 -0
  30. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/__init__.py +0 -0
  31. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/__main__.py +0 -0
  32. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/agent/__init__.py +0 -0
  33. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/agent/cleanup.py +0 -0
  34. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/agent/repl.py +0 -0
  35. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/agent/safety.py +0 -0
  36. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/agent/tools.py +0 -0
  37. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/architect.py +0 -0
  38. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/auditor.py +0 -0
  39. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/auth.py +0 -0
  40. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/commands/__init__.py +0 -0
  41. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/compressor.py +0 -0
  42. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/config.py +0 -0
  43. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/credit_analyzer.py +0 -0
  44. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/credits.py +0 -0
  45. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/differ.py +0 -0
  46. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/doctor.py +0 -0
  47. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/epistemic/__init__.py +0 -0
  48. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/epistemic/belief.py +0 -0
  49. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/epistemic/certainty.py +0 -0
  50. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/epistemic/failure_graph.py +0 -0
  51. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/epistemic/recovery.py +0 -0
  52. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/epistemic/stress_tester.py +0 -0
  53. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/executor.py +0 -0
  54. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/exporter.py +0 -0
  55. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/gui/__init__.py +0 -0
  56. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/gui/app.py +0 -0
  57. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/gui/main_window.py +0 -0
  58. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/gui/session_tab.py +0 -0
  59. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/gui/theme.py +0 -0
  60. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/gui/widgets/__init__.py +0 -0
  61. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/gui/widgets/chat_view.py +0 -0
  62. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/gui/widgets/input_bar.py +0 -0
  63. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/gui/widgets/provider_bar.py +0 -0
  64. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/gui/widgets/token_meter.py +0 -0
  65. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/gui/widgets/tool_panel.py +0 -0
  66. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/gui/widgets/update_checker.py +0 -0
  67. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/gui/worker.py +0 -0
  68. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/importer.py +0 -0
  69. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/integrations/__init__.py +0 -0
  70. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/integrations/aider.py +0 -0
  71. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/integrations/base.py +0 -0
  72. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/integrations/claude_code.py +0 -0
  73. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/integrations/copilot.py +0 -0
  74. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/integrations/cursor.py +0 -0
  75. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/integrations/gemini.py +0 -0
  76. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/integrations/warp.py +0 -0
  77. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/integrations/windsurf.py +0 -0
  78. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/languages.py +0 -0
  79. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/ledger.py +0 -0
  80. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/patent.py +0 -0
  81. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/phase.py +0 -0
  82. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/plugins.py +0 -0
  83. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/profiles.py +0 -0
  84. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/rate_limits.py +0 -0
  85. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/releaser.py +0 -0
  86. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/requirements.py +0 -0
  87. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/requirements_parser.py +0 -0
  88. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/retrieval.py +0 -0
  89. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/scaffolder.py +0 -0
  90. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/serve.py +0 -0
  91. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/session.py +0 -0
  92. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/agents.md.j2 +0 -0
  93. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/community/bug_report.md.j2 +0 -0
  94. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/community/code_of_conduct.md.j2 +0 -0
  95. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/community/contributing.md.j2 +0 -0
  96. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/community/feature_request.md.j2 +0 -0
  97. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/community/license-Apache-2.0.j2 +0 -0
  98. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/community/license-MIT.j2 +0 -0
  99. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/community/pull_request_template.md.j2 +0 -0
  100. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/community/security.md.j2 +0 -0
  101. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/docs/architecture.md.j2 +0 -0
  102. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/docs/mkdocs.yml.j2 +0 -0
  103. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/docs/readthedocs.yaml.j2 +0 -0
  104. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/docs/requirements.md.j2 +0 -0
  105. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/docs/test-spec.md.j2 +0 -0
  106. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/editorconfig.j2 +0 -0
  107. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/gitattributes.j2 +0 -0
  108. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/gitignore.j2 +0 -0
  109. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/go/go.mod.j2 +0 -0
  110. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/go/main.go.j2 +0 -0
  111. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/governance/belief-registry.md.j2 +0 -0
  112. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/governance/context-budget.md.j2 +0 -0
  113. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/governance/drift-metrics.md.j2 +0 -0
  114. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/governance/epistemic-axioms.md.j2 +0 -0
  115. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/governance/failure-modes.md.j2 +0 -0
  116. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/governance/lifecycle.md.j2 +0 -0
  117. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/governance/roles.md.j2 +0 -0
  118. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/governance/rules.md.j2 +0 -0
  119. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/governance/session-protocol.md.j2 +0 -0
  120. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/governance/uncertainty-map.md.j2 +0 -0
  121. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/governance/verification.md.j2 +0 -0
  122. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/js/package.json.j2 +0 -0
  123. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/ledger.md.j2 +0 -0
  124. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/python/cli.py.j2 +0 -0
  125. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/python/init.py.j2 +0 -0
  126. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/python/pyproject.toml.j2 +0 -0
  127. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/readme.md.j2 +0 -0
  128. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/rust/Cargo.toml.j2 +0 -0
  129. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/rust/main.rs.j2 +0 -0
  130. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/scripts/exec.cmd.j2 +0 -0
  131. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/scripts/exec.sh.j2 +0 -0
  132. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/scripts/run.cmd.j2 +0 -0
  133. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/scripts/run.sh.j2 +0 -0
  134. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/scripts/setup.cmd.j2 +0 -0
  135. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/scripts/setup.sh.j2 +0 -0
  136. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/workflows/release.yml.j2 +0 -0
  137. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/tool_installer.py +0 -0
  138. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/toolrules.py +0 -0
  139. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/tools.py +0 -0
  140. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/trace.py +0 -0
  141. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/updater.py +0 -0
  142. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/upgrader.py +0 -0
  143. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/validator.py +0 -0
  144. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/vcs/__init__.py +0 -0
  145. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/vcs/base.py +0 -0
  146. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/vcs/bitbucket.py +0 -0
  147. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/vcs/github.py +0 -0
  148. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/vcs/gitlab.py +0 -0
  149. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/vcs_commands.py +0 -0
  150. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/wireframes.py +0 -0
  151. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/workspace.py +0 -0
  152. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith.egg-info/dependency_links.txt +0 -0
  153. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith.egg-info/entry_points.txt +0 -0
  154. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith.egg-info/requires.txt +0 -0
  155. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith.egg-info/top_level.txt +0 -0
  156. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/tests/test_CMD_001.py +0 -0
  157. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/tests/test_auditor.py +0 -0
  158. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/tests/test_cli.py +0 -0
  159. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/tests/test_compressor.py +0 -0
  160. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/tests/test_epistemic.py +0 -0
  161. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/tests/test_importer.py +0 -0
  162. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/tests/test_integrations.py +0 -0
  163. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/tests/test_nexus.py +0 -0
  164. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/tests/test_rate_limits.py +0 -0
  165. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/tests/test_scaffolder.py +0 -0
  166. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/tests/test_smoke.py +0 -0
  167. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/tests/test_tools.py +0 -0
  168. {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/tests/test_validator.py +0 -0
  169. {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.dev221
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.dev221"
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
- # New modules that use dynamic typing patterns incompatible with strict mypy.
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"]
@@ -5,7 +5,7 @@ import subprocess
5
5
  from pathlib import Path
6
6
 
7
7
 
8
- def generate_index(cwd: str = None):
8
+ def generate_index(cwd: str | None = None) -> None:
9
9
  """Generate repository index into .repo-index/"""
10
10
  if cwd is None:
11
11
  cwd = os.getcwd()
@@ -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 treat reaching a non-empty summary that includes the
211
- ``Next action:`` section as equilibrium per REQ-073, and parse
212
- ``Files changed:`` and ``Test results:`` if present. The confidence
213
- is conservative (0.85 for a complete contract response, 0.4 for a
214
- partial one) and is meant to be replaced by a real verifier signal
215
- once one is wired in.
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"]