superqode 0.1.5__py3-none-any.whl

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 (288) hide show
  1. superqode/__init__.py +33 -0
  2. superqode/acp/__init__.py +23 -0
  3. superqode/acp/client.py +913 -0
  4. superqode/acp/permission_screen.py +457 -0
  5. superqode/acp/types.py +480 -0
  6. superqode/acp_discovery.py +856 -0
  7. superqode/agent/__init__.py +22 -0
  8. superqode/agent/edit_strategies.py +334 -0
  9. superqode/agent/loop.py +892 -0
  10. superqode/agent/qe_report_templates.py +39 -0
  11. superqode/agent/system_prompts.py +353 -0
  12. superqode/agent_output.py +721 -0
  13. superqode/agent_stream.py +953 -0
  14. superqode/agents/__init__.py +59 -0
  15. superqode/agents/acp_registry.py +305 -0
  16. superqode/agents/client.py +249 -0
  17. superqode/agents/data/augmentcode.com.toml +51 -0
  18. superqode/agents/data/cagent.dev.toml +51 -0
  19. superqode/agents/data/claude.com.toml +60 -0
  20. superqode/agents/data/codeassistant.dev.toml +51 -0
  21. superqode/agents/data/codex.openai.com.toml +57 -0
  22. superqode/agents/data/fastagent.ai.toml +66 -0
  23. superqode/agents/data/geminicli.com.toml +77 -0
  24. superqode/agents/data/goose.block.xyz.toml +54 -0
  25. superqode/agents/data/junie.jetbrains.com.toml +56 -0
  26. superqode/agents/data/kimi.moonshot.cn.toml +57 -0
  27. superqode/agents/data/llmlingagent.dev.toml +51 -0
  28. superqode/agents/data/molt.bot.toml +49 -0
  29. superqode/agents/data/opencode.ai.toml +60 -0
  30. superqode/agents/data/stakpak.dev.toml +51 -0
  31. superqode/agents/data/vtcode.dev.toml +51 -0
  32. superqode/agents/discovery.py +266 -0
  33. superqode/agents/messaging.py +160 -0
  34. superqode/agents/persona.py +166 -0
  35. superqode/agents/registry.py +421 -0
  36. superqode/agents/schema.py +72 -0
  37. superqode/agents/unified.py +367 -0
  38. superqode/app/__init__.py +111 -0
  39. superqode/app/constants.py +314 -0
  40. superqode/app/css.py +366 -0
  41. superqode/app/models.py +118 -0
  42. superqode/app/suggester.py +125 -0
  43. superqode/app/widgets.py +1591 -0
  44. superqode/app_enhanced.py +399 -0
  45. superqode/app_main.py +17187 -0
  46. superqode/approval.py +312 -0
  47. superqode/atomic.py +296 -0
  48. superqode/commands/__init__.py +1 -0
  49. superqode/commands/acp.py +965 -0
  50. superqode/commands/agents.py +180 -0
  51. superqode/commands/auth.py +278 -0
  52. superqode/commands/config.py +374 -0
  53. superqode/commands/init.py +826 -0
  54. superqode/commands/providers.py +819 -0
  55. superqode/commands/qe.py +1145 -0
  56. superqode/commands/roles.py +380 -0
  57. superqode/commands/serve.py +172 -0
  58. superqode/commands/suggestions.py +127 -0
  59. superqode/commands/superqe.py +460 -0
  60. superqode/config/__init__.py +51 -0
  61. superqode/config/loader.py +812 -0
  62. superqode/config/schema.py +498 -0
  63. superqode/core/__init__.py +111 -0
  64. superqode/core/roles.py +281 -0
  65. superqode/danger.py +386 -0
  66. superqode/data/superqode-template.yaml +1522 -0
  67. superqode/design_system.py +1080 -0
  68. superqode/dialogs/__init__.py +6 -0
  69. superqode/dialogs/base.py +39 -0
  70. superqode/dialogs/model.py +130 -0
  71. superqode/dialogs/provider.py +870 -0
  72. superqode/diff_view.py +919 -0
  73. superqode/enterprise.py +21 -0
  74. superqode/evaluation/__init__.py +25 -0
  75. superqode/evaluation/adapters.py +93 -0
  76. superqode/evaluation/behaviors.py +89 -0
  77. superqode/evaluation/engine.py +209 -0
  78. superqode/evaluation/scenarios.py +96 -0
  79. superqode/execution/__init__.py +36 -0
  80. superqode/execution/linter.py +538 -0
  81. superqode/execution/modes.py +347 -0
  82. superqode/execution/resolver.py +283 -0
  83. superqode/execution/runner.py +642 -0
  84. superqode/file_explorer.py +811 -0
  85. superqode/file_viewer.py +471 -0
  86. superqode/flash.py +183 -0
  87. superqode/guidance/__init__.py +58 -0
  88. superqode/guidance/config.py +203 -0
  89. superqode/guidance/prompts.py +71 -0
  90. superqode/harness/__init__.py +54 -0
  91. superqode/harness/accelerator.py +291 -0
  92. superqode/harness/config.py +319 -0
  93. superqode/harness/validator.py +147 -0
  94. superqode/history.py +279 -0
  95. superqode/integrations/superopt_runner.py +124 -0
  96. superqode/logging/__init__.py +49 -0
  97. superqode/logging/adapters.py +219 -0
  98. superqode/logging/formatter.py +923 -0
  99. superqode/logging/integration.py +341 -0
  100. superqode/logging/sinks.py +170 -0
  101. superqode/logging/unified_log.py +417 -0
  102. superqode/lsp/__init__.py +26 -0
  103. superqode/lsp/client.py +544 -0
  104. superqode/main.py +1069 -0
  105. superqode/mcp/__init__.py +89 -0
  106. superqode/mcp/auth_storage.py +380 -0
  107. superqode/mcp/client.py +1236 -0
  108. superqode/mcp/config.py +319 -0
  109. superqode/mcp/integration.py +337 -0
  110. superqode/mcp/oauth.py +436 -0
  111. superqode/mcp/oauth_callback.py +385 -0
  112. superqode/mcp/types.py +290 -0
  113. superqode/memory/__init__.py +31 -0
  114. superqode/memory/feedback.py +342 -0
  115. superqode/memory/store.py +522 -0
  116. superqode/notifications.py +369 -0
  117. superqode/optimization/__init__.py +5 -0
  118. superqode/optimization/config.py +33 -0
  119. superqode/permissions/__init__.py +25 -0
  120. superqode/permissions/rules.py +488 -0
  121. superqode/plan.py +323 -0
  122. superqode/providers/__init__.py +33 -0
  123. superqode/providers/gateway/__init__.py +165 -0
  124. superqode/providers/gateway/base.py +228 -0
  125. superqode/providers/gateway/litellm_gateway.py +1170 -0
  126. superqode/providers/gateway/openresponses_gateway.py +436 -0
  127. superqode/providers/health.py +297 -0
  128. superqode/providers/huggingface/__init__.py +74 -0
  129. superqode/providers/huggingface/downloader.py +472 -0
  130. superqode/providers/huggingface/endpoints.py +442 -0
  131. superqode/providers/huggingface/hub.py +531 -0
  132. superqode/providers/huggingface/inference.py +394 -0
  133. superqode/providers/huggingface/transformers_runner.py +516 -0
  134. superqode/providers/local/__init__.py +100 -0
  135. superqode/providers/local/base.py +438 -0
  136. superqode/providers/local/discovery.py +418 -0
  137. superqode/providers/local/lmstudio.py +256 -0
  138. superqode/providers/local/mlx.py +457 -0
  139. superqode/providers/local/ollama.py +486 -0
  140. superqode/providers/local/sglang.py +268 -0
  141. superqode/providers/local/tgi.py +260 -0
  142. superqode/providers/local/tool_support.py +477 -0
  143. superqode/providers/local/vllm.py +258 -0
  144. superqode/providers/manager.py +1338 -0
  145. superqode/providers/models.py +1016 -0
  146. superqode/providers/models_dev.py +578 -0
  147. superqode/providers/openresponses/__init__.py +87 -0
  148. superqode/providers/openresponses/converters/__init__.py +17 -0
  149. superqode/providers/openresponses/converters/messages.py +343 -0
  150. superqode/providers/openresponses/converters/tools.py +268 -0
  151. superqode/providers/openresponses/schema/__init__.py +56 -0
  152. superqode/providers/openresponses/schema/models.py +585 -0
  153. superqode/providers/openresponses/streaming/__init__.py +5 -0
  154. superqode/providers/openresponses/streaming/parser.py +338 -0
  155. superqode/providers/openresponses/tools/__init__.py +21 -0
  156. superqode/providers/openresponses/tools/apply_patch.py +352 -0
  157. superqode/providers/openresponses/tools/code_interpreter.py +290 -0
  158. superqode/providers/openresponses/tools/file_search.py +333 -0
  159. superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
  160. superqode/providers/registry.py +716 -0
  161. superqode/providers/usage.py +332 -0
  162. superqode/pure_mode.py +384 -0
  163. superqode/qr/__init__.py +23 -0
  164. superqode/qr/dashboard.py +781 -0
  165. superqode/qr/generator.py +1018 -0
  166. superqode/qr/templates.py +135 -0
  167. superqode/safety/__init__.py +41 -0
  168. superqode/safety/sandbox.py +413 -0
  169. superqode/safety/warnings.py +256 -0
  170. superqode/server/__init__.py +33 -0
  171. superqode/server/lsp_server.py +775 -0
  172. superqode/server/web.py +250 -0
  173. superqode/session/__init__.py +25 -0
  174. superqode/session/persistence.py +580 -0
  175. superqode/session/sharing.py +477 -0
  176. superqode/session.py +475 -0
  177. superqode/sidebar.py +2991 -0
  178. superqode/stream_view.py +648 -0
  179. superqode/styles/__init__.py +3 -0
  180. superqode/superqe/__init__.py +184 -0
  181. superqode/superqe/acp_runner.py +1064 -0
  182. superqode/superqe/constitution/__init__.py +62 -0
  183. superqode/superqe/constitution/evaluator.py +308 -0
  184. superqode/superqe/constitution/loader.py +432 -0
  185. superqode/superqe/constitution/schema.py +250 -0
  186. superqode/superqe/events.py +591 -0
  187. superqode/superqe/frameworks/__init__.py +65 -0
  188. superqode/superqe/frameworks/base.py +234 -0
  189. superqode/superqe/frameworks/e2e.py +263 -0
  190. superqode/superqe/frameworks/executor.py +237 -0
  191. superqode/superqe/frameworks/javascript.py +409 -0
  192. superqode/superqe/frameworks/python.py +373 -0
  193. superqode/superqe/frameworks/registry.py +92 -0
  194. superqode/superqe/mcp_tools/__init__.py +47 -0
  195. superqode/superqe/mcp_tools/core_tools.py +418 -0
  196. superqode/superqe/mcp_tools/registry.py +230 -0
  197. superqode/superqe/mcp_tools/testing_tools.py +167 -0
  198. superqode/superqe/noise.py +89 -0
  199. superqode/superqe/orchestrator.py +778 -0
  200. superqode/superqe/roles.py +609 -0
  201. superqode/superqe/session.py +713 -0
  202. superqode/superqe/skills/__init__.py +57 -0
  203. superqode/superqe/skills/base.py +106 -0
  204. superqode/superqe/skills/core_skills.py +899 -0
  205. superqode/superqe/skills/registry.py +90 -0
  206. superqode/superqe/verifier.py +101 -0
  207. superqode/superqe_cli.py +76 -0
  208. superqode/tool_call.py +358 -0
  209. superqode/tools/__init__.py +93 -0
  210. superqode/tools/agent_tools.py +496 -0
  211. superqode/tools/base.py +324 -0
  212. superqode/tools/batch_tool.py +133 -0
  213. superqode/tools/diagnostics.py +311 -0
  214. superqode/tools/edit_tools.py +653 -0
  215. superqode/tools/enhanced_base.py +515 -0
  216. superqode/tools/file_tools.py +269 -0
  217. superqode/tools/file_tracking.py +45 -0
  218. superqode/tools/lsp_tools.py +610 -0
  219. superqode/tools/network_tools.py +350 -0
  220. superqode/tools/permissions.py +400 -0
  221. superqode/tools/question_tool.py +324 -0
  222. superqode/tools/search_tools.py +598 -0
  223. superqode/tools/shell_tools.py +259 -0
  224. superqode/tools/todo_tools.py +121 -0
  225. superqode/tools/validation.py +80 -0
  226. superqode/tools/web_tools.py +639 -0
  227. superqode/tui.py +1152 -0
  228. superqode/tui_integration.py +875 -0
  229. superqode/tui_widgets/__init__.py +27 -0
  230. superqode/tui_widgets/widgets/__init__.py +18 -0
  231. superqode/tui_widgets/widgets/progress.py +185 -0
  232. superqode/tui_widgets/widgets/tool_display.py +188 -0
  233. superqode/undo_manager.py +574 -0
  234. superqode/utils/__init__.py +5 -0
  235. superqode/utils/error_handling.py +323 -0
  236. superqode/utils/fuzzy.py +257 -0
  237. superqode/widgets/__init__.py +477 -0
  238. superqode/widgets/agent_collab.py +390 -0
  239. superqode/widgets/agent_store.py +936 -0
  240. superqode/widgets/agent_switcher.py +395 -0
  241. superqode/widgets/animation_manager.py +284 -0
  242. superqode/widgets/code_context.py +356 -0
  243. superqode/widgets/command_palette.py +412 -0
  244. superqode/widgets/connection_status.py +537 -0
  245. superqode/widgets/conversation_history.py +470 -0
  246. superqode/widgets/diff_indicator.py +155 -0
  247. superqode/widgets/enhanced_status_bar.py +385 -0
  248. superqode/widgets/enhanced_toast.py +476 -0
  249. superqode/widgets/file_browser.py +809 -0
  250. superqode/widgets/file_reference.py +585 -0
  251. superqode/widgets/issue_timeline.py +340 -0
  252. superqode/widgets/leader_key.py +264 -0
  253. superqode/widgets/mode_switcher.py +445 -0
  254. superqode/widgets/model_picker.py +234 -0
  255. superqode/widgets/permission_preview.py +1205 -0
  256. superqode/widgets/prompt.py +358 -0
  257. superqode/widgets/provider_connect.py +725 -0
  258. superqode/widgets/pty_shell.py +587 -0
  259. superqode/widgets/qe_dashboard.py +321 -0
  260. superqode/widgets/resizable_sidebar.py +377 -0
  261. superqode/widgets/response_changes.py +218 -0
  262. superqode/widgets/response_display.py +528 -0
  263. superqode/widgets/rich_tool_display.py +613 -0
  264. superqode/widgets/sidebar_panels.py +1180 -0
  265. superqode/widgets/slash_complete.py +356 -0
  266. superqode/widgets/split_view.py +612 -0
  267. superqode/widgets/status_bar.py +273 -0
  268. superqode/widgets/superqode_display.py +786 -0
  269. superqode/widgets/thinking_display.py +815 -0
  270. superqode/widgets/throbber.py +87 -0
  271. superqode/widgets/toast.py +206 -0
  272. superqode/widgets/unified_output.py +1073 -0
  273. superqode/workspace/__init__.py +75 -0
  274. superqode/workspace/artifacts.py +472 -0
  275. superqode/workspace/coordinator.py +353 -0
  276. superqode/workspace/diff_tracker.py +429 -0
  277. superqode/workspace/git_guard.py +373 -0
  278. superqode/workspace/git_snapshot.py +526 -0
  279. superqode/workspace/manager.py +750 -0
  280. superqode/workspace/snapshot.py +357 -0
  281. superqode/workspace/watcher.py +535 -0
  282. superqode/workspace/worktree.py +440 -0
  283. superqode-0.1.5.dist-info/METADATA +204 -0
  284. superqode-0.1.5.dist-info/RECORD +288 -0
  285. superqode-0.1.5.dist-info/WHEEL +5 -0
  286. superqode-0.1.5.dist-info/entry_points.txt +3 -0
  287. superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
  288. superqode-0.1.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,319 @@
1
+ """
2
+ Harness Configuration - YAML-driven validation settings.
3
+
4
+ All configuration comes from superqode.yaml, no external files.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from enum import Enum
9
+ from pathlib import Path
10
+ from typing import Any, Dict, List, Optional
11
+ import logging
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class ValidationCategory(Enum):
17
+ """Category of validation."""
18
+
19
+ STRUCTURAL = "structural" # JSON, YAML, TOML parsing
20
+ FUNCTIONAL = "functional" # Type checking, linting
21
+ STYLISTIC = "stylistic" # Formatting, style rules
22
+
23
+
24
+ @dataclass
25
+ class ToolConfig:
26
+ """Configuration for a validation tool."""
27
+
28
+ name: str
29
+ enabled: bool = True
30
+ args: List[str] = field(default_factory=list)
31
+ timeout_seconds: int = 10
32
+ category: ValidationCategory = ValidationCategory.FUNCTIONAL
33
+
34
+
35
+ @dataclass
36
+ class LanguageValidatorConfig:
37
+ """Configuration for a language's validators."""
38
+
39
+ enabled: bool = True
40
+ tools: List[ToolConfig] = field(default_factory=list)
41
+ extensions: List[str] = field(default_factory=list)
42
+
43
+
44
+ @dataclass
45
+ class StructuralValidatorConfig:
46
+ """Configuration for structural validators."""
47
+
48
+ enabled: bool = True
49
+ formats: List[str] = field(default_factory=lambda: ["json", "yaml", "toml"])
50
+
51
+
52
+ @dataclass
53
+ class CustomHarnessStep:
54
+ """Custom harness step defined by user command."""
55
+
56
+ name: str
57
+ command: str
58
+ enabled: bool = True
59
+ timeout_seconds: int = 300
60
+
61
+
62
+ @dataclass
63
+ class HarnessConfig:
64
+ """
65
+ Complete harness configuration from superqode.yaml.
66
+
67
+ Supports:
68
+ - Structural validators (JSON, YAML, TOML)
69
+ - Language-specific validators (Python, JS, Go, etc.)
70
+ - Custom tool configurations
71
+ - Timeout and retry settings
72
+ """
73
+
74
+ enabled: bool = True
75
+ timeout_seconds: int = 30
76
+ fail_on_error: bool = False # Should harness failures block QE?
77
+
78
+ # Structural validators
79
+ structural: StructuralValidatorConfig = field(default_factory=StructuralValidatorConfig)
80
+
81
+ # Language validators
82
+ python: LanguageValidatorConfig = field(
83
+ default_factory=lambda: LanguageValidatorConfig(
84
+ enabled=True,
85
+ extensions=[".py", ".pyi"],
86
+ tools=[
87
+ ToolConfig(name="ruff", args=["check", "--quiet"]),
88
+ ToolConfig(name="mypy", args=["--no-error-summary"], timeout_seconds=20),
89
+ ],
90
+ )
91
+ )
92
+
93
+ javascript: LanguageValidatorConfig = field(
94
+ default_factory=lambda: LanguageValidatorConfig(
95
+ enabled=True,
96
+ extensions=[".js", ".jsx", ".mjs", ".cjs"],
97
+ tools=[
98
+ ToolConfig(name="eslint", args=["--max-warnings", "0"]),
99
+ ],
100
+ )
101
+ )
102
+
103
+ typescript: LanguageValidatorConfig = field(
104
+ default_factory=lambda: LanguageValidatorConfig(
105
+ enabled=True,
106
+ extensions=[".ts", ".tsx"],
107
+ tools=[
108
+ ToolConfig(name="tsc", args=["--noEmit"], timeout_seconds=30),
109
+ ToolConfig(name="eslint", args=["--max-warnings", "0"]),
110
+ ],
111
+ )
112
+ )
113
+
114
+ go: LanguageValidatorConfig = field(
115
+ default_factory=lambda: LanguageValidatorConfig(
116
+ enabled=True,
117
+ extensions=[".go"],
118
+ tools=[
119
+ ToolConfig(name="go", args=["vet"]),
120
+ ToolConfig(name="golangci-lint", args=["run"], timeout_seconds=20),
121
+ ],
122
+ )
123
+ )
124
+
125
+ rust: LanguageValidatorConfig = field(
126
+ default_factory=lambda: LanguageValidatorConfig(
127
+ enabled=True,
128
+ extensions=[".rs"],
129
+ tools=[
130
+ ToolConfig(name="cargo", args=["check", "--quiet"], timeout_seconds=30),
131
+ ],
132
+ )
133
+ )
134
+
135
+ shell: LanguageValidatorConfig = field(
136
+ default_factory=lambda: LanguageValidatorConfig(
137
+ enabled=True,
138
+ extensions=[".sh", ".bash"],
139
+ tools=[
140
+ ToolConfig(name="shellcheck", args=["-f", "gcc"]),
141
+ ],
142
+ )
143
+ )
144
+
145
+ yaml_lint: LanguageValidatorConfig = field(
146
+ default_factory=lambda: LanguageValidatorConfig(
147
+ enabled=True,
148
+ extensions=[".yml", ".yaml"],
149
+ tools=[
150
+ ToolConfig(
151
+ name="yamllint", args=["-f", "parsable"], category=ValidationCategory.STYLISTIC
152
+ ),
153
+ ],
154
+ )
155
+ )
156
+
157
+ markdown: LanguageValidatorConfig = field(
158
+ default_factory=lambda: LanguageValidatorConfig(
159
+ enabled=True,
160
+ extensions=[".md"],
161
+ tools=[
162
+ ToolConfig(name="markdownlint", args=[], category=ValidationCategory.STYLISTIC),
163
+ ],
164
+ )
165
+ )
166
+
167
+ dockerfile: LanguageValidatorConfig = field(
168
+ default_factory=lambda: LanguageValidatorConfig(
169
+ enabled=True,
170
+ extensions=[], # Matched by filename
171
+ tools=[
172
+ ToolConfig(name="hadolint", args=[]),
173
+ ],
174
+ )
175
+ )
176
+
177
+ # Custom command-based steps (BYOH)
178
+ custom_steps: List[CustomHarnessStep] = field(default_factory=list)
179
+
180
+ @classmethod
181
+ def from_yaml_dict(cls, data: Dict[str, Any]) -> "HarnessConfig":
182
+ """Create HarnessConfig from YAML dict (superqode.qe.harness section)."""
183
+ if not data:
184
+ return cls()
185
+
186
+ config = cls(
187
+ enabled=data.get("enabled", True),
188
+ timeout_seconds=data.get("timeout_seconds", 30),
189
+ fail_on_error=data.get("fail_on_error", False),
190
+ )
191
+
192
+ # Parse structural config
193
+ if "structural" in data:
194
+ config.structural = StructuralValidatorConfig(
195
+ enabled=data["structural"].get("enabled", True),
196
+ formats=data["structural"].get("formats", ["json", "yaml", "toml"]),
197
+ )
198
+
199
+ # Parse language configs
200
+ for lang in [
201
+ "python",
202
+ "javascript",
203
+ "typescript",
204
+ "go",
205
+ "rust",
206
+ "shell",
207
+ "yaml_lint",
208
+ "markdown",
209
+ "dockerfile",
210
+ ]:
211
+ yaml_key = lang.replace("_", "-")
212
+ if yaml_key in data or lang in data:
213
+ lang_data = data.get(yaml_key) or data.get(lang, {})
214
+ lang_config = getattr(config, lang)
215
+
216
+ if isinstance(lang_data, dict):
217
+ lang_config.enabled = lang_data.get("enabled", True)
218
+
219
+ if "extensions" in lang_data:
220
+ lang_config.extensions = lang_data["extensions"]
221
+
222
+ if "tools" in lang_data:
223
+ lang_config.tools = []
224
+ for tool_data in lang_data["tools"]:
225
+ if isinstance(tool_data, str):
226
+ # Simple string: just tool name
227
+ lang_config.tools.append(ToolConfig(name=tool_data))
228
+ elif isinstance(tool_data, dict):
229
+ lang_config.tools.append(
230
+ ToolConfig(
231
+ name=tool_data.get("name", tool_data.get("tool", "")),
232
+ enabled=tool_data.get("enabled", True),
233
+ args=tool_data.get("args", []),
234
+ timeout_seconds=tool_data.get("timeout", 10),
235
+ )
236
+ )
237
+
238
+ # Parse custom steps
239
+ custom_steps_data = data.get("custom_steps") or data.get("custom-steps") or []
240
+ for step_data in custom_steps_data:
241
+ if isinstance(step_data, str):
242
+ config.custom_steps.append(
243
+ CustomHarnessStep(
244
+ name=step_data,
245
+ command=step_data,
246
+ )
247
+ )
248
+ elif isinstance(step_data, dict):
249
+ config.custom_steps.append(
250
+ CustomHarnessStep(
251
+ name=step_data.get("name", step_data.get("command", "custom-step")),
252
+ command=step_data.get("command", ""),
253
+ enabled=step_data.get("enabled", True),
254
+ timeout_seconds=step_data.get("timeout", 300),
255
+ )
256
+ )
257
+
258
+ return config
259
+
260
+ def get_validators_for_file(self, file_path: Path) -> List["LanguageValidatorConfig"]:
261
+ """Get all applicable validators for a file."""
262
+ validators = []
263
+ ext = file_path.suffix.lower()
264
+ filename = file_path.name.lower()
265
+
266
+ # Check each language
267
+ for lang in [
268
+ "python",
269
+ "javascript",
270
+ "typescript",
271
+ "go",
272
+ "rust",
273
+ "shell",
274
+ "yaml_lint",
275
+ "markdown",
276
+ "dockerfile",
277
+ ]:
278
+ lang_config = getattr(self, lang)
279
+ if not lang_config.enabled:
280
+ continue
281
+
282
+ # Check extension match
283
+ if ext in lang_config.extensions:
284
+ validators.append(lang_config)
285
+
286
+ # Special case: Dockerfile
287
+ if lang == "dockerfile" and (
288
+ filename == "dockerfile" or filename.startswith("dockerfile.")
289
+ ):
290
+ validators.append(lang_config)
291
+
292
+ return validators
293
+
294
+
295
+ def load_harness_config(project_root: Path) -> HarnessConfig:
296
+ """
297
+ Load harness configuration from superqode.yaml.
298
+
299
+ Looks for: superqode.qe.harness section
300
+ """
301
+ import yaml
302
+
303
+ yaml_path = project_root / "superqode.yaml"
304
+ if not yaml_path.exists():
305
+ logger.debug("No superqode.yaml found, using default harness config")
306
+ return HarnessConfig()
307
+
308
+ try:
309
+ with open(yaml_path) as f:
310
+ data = yaml.safe_load(f)
311
+
312
+ # Navigate to superqode.qe.harness
313
+ harness_data = data.get("superqode", {}).get("qe", {}).get("harness", {})
314
+
315
+ return HarnessConfig.from_yaml_dict(harness_data)
316
+
317
+ except Exception as e:
318
+ logger.warning(f"Failed to load harness config: {e}")
319
+ return HarnessConfig()
@@ -0,0 +1,147 @@
1
+ """
2
+ Patch Harness Validator - OSS command-based harness.
3
+
4
+ Runs user-provided commands from superqode.yaml (BYOH).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import subprocess
10
+ from dataclasses import dataclass, field
11
+ from pathlib import Path
12
+ from typing import Dict, List, Optional
13
+ import time
14
+
15
+ from .config import HarnessConfig, ValidationCategory
16
+
17
+
18
+ @dataclass
19
+ class HarnessFinding:
20
+ """A finding from the harness validation."""
21
+
22
+ tool: str
23
+ category: ValidationCategory
24
+ file: Optional[Path]
25
+ message: str
26
+ line: Optional[int] = None
27
+ column: Optional[int] = None
28
+ severity: str = "error"
29
+
30
+ def to_dict(self) -> Dict[str, object]:
31
+ return {
32
+ "tool": self.tool,
33
+ "category": self.category.value,
34
+ "file": str(self.file) if self.file else None,
35
+ "message": self.message,
36
+ "line": self.line,
37
+ "column": self.column,
38
+ "severity": self.severity,
39
+ }
40
+
41
+
42
+ @dataclass
43
+ class HarnessResult:
44
+ """Result of running the harness on a patch."""
45
+
46
+ success: bool
47
+ findings: List[HarnessFinding] = field(default_factory=list)
48
+ tools_run: List[str] = field(default_factory=list)
49
+ duration_seconds: float = 0.0
50
+ files_validated: int = 0
51
+
52
+ @property
53
+ def error_count(self) -> int:
54
+ return sum(1 for f in self.findings if f.severity == "error")
55
+
56
+ @property
57
+ def warning_count(self) -> int:
58
+ return sum(1 for f in self.findings if f.severity == "warning")
59
+
60
+ def to_dict(self) -> Dict[str, object]:
61
+ return {
62
+ "success": self.success,
63
+ "error_count": self.error_count,
64
+ "warning_count": self.warning_count,
65
+ "tools_run": self.tools_run,
66
+ "duration_seconds": self.duration_seconds,
67
+ "files_validated": self.files_validated,
68
+ "findings": [f.to_dict() for f in self.findings],
69
+ }
70
+
71
+
72
+ class PatchHarness:
73
+ """Command-based harness runner for OSS."""
74
+
75
+ def __init__(
76
+ self,
77
+ project_root: Path,
78
+ config: Optional[HarnessConfig] = None,
79
+ ):
80
+ from .config import load_harness_config
81
+
82
+ self.project_root = project_root.resolve()
83
+ self.config = config or load_harness_config(project_root)
84
+
85
+ async def validate_changes(
86
+ self,
87
+ _changes: Dict[Path, str],
88
+ timeout_override: Optional[int] = None,
89
+ ) -> HarnessResult:
90
+ if not self.config.enabled:
91
+ return HarnessResult(success=True)
92
+
93
+ if not self.config.custom_steps:
94
+ return HarnessResult(success=True)
95
+
96
+ start_time = time.monotonic()
97
+ findings: List[HarnessFinding] = []
98
+ tools_run: List[str] = []
99
+
100
+ for step in self.config.custom_steps:
101
+ if not step.enabled:
102
+ continue
103
+
104
+ tools_run.append(step.name)
105
+ timeout = timeout_override or step.timeout_seconds
106
+ try:
107
+ result = subprocess.run(
108
+ step.command,
109
+ cwd=self.project_root,
110
+ shell=True,
111
+ capture_output=True,
112
+ text=True,
113
+ timeout=timeout,
114
+ )
115
+ if result.returncode != 0:
116
+ stderr = result.stderr.strip()
117
+ message = stderr or result.stdout.strip() or "Command failed"
118
+ findings.append(
119
+ HarnessFinding(
120
+ tool=step.name,
121
+ category=ValidationCategory.FUNCTIONAL,
122
+ file=None,
123
+ message=message,
124
+ severity="error",
125
+ )
126
+ )
127
+ except subprocess.TimeoutExpired:
128
+ findings.append(
129
+ HarnessFinding(
130
+ tool=step.name,
131
+ category=ValidationCategory.FUNCTIONAL,
132
+ file=None,
133
+ message=f"Command timed out after {timeout}s",
134
+ severity="warning",
135
+ )
136
+ )
137
+
138
+ duration = time.monotonic() - start_time
139
+ success = all(f.severity != "error" for f in findings)
140
+
141
+ return HarnessResult(
142
+ success=success,
143
+ findings=findings,
144
+ tools_run=tools_run,
145
+ duration_seconds=duration,
146
+ files_validated=0,
147
+ )