devtorch-core 3.0.1__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 (193) hide show
  1. devtorch_core/__init__.py +158 -0
  2. devtorch_core/aggphi_textual.py +275 -0
  3. devtorch_core/alerts/__init__.py +23 -0
  4. devtorch_core/alerts/base.py +46 -0
  5. devtorch_core/alerts/config.py +60 -0
  6. devtorch_core/alerts/dispatcher.py +110 -0
  7. devtorch_core/alerts/jira.py +96 -0
  8. devtorch_core/alerts/linear.py +72 -0
  9. devtorch_core/alerts/pagerduty.py +66 -0
  10. devtorch_core/alerts/slack.py +81 -0
  11. devtorch_core/alerts/teams.py +70 -0
  12. devtorch_core/audit/__init__.py +43 -0
  13. devtorch_core/audit/exporter.py +297 -0
  14. devtorch_core/audit/privacy.py +101 -0
  15. devtorch_core/audit/scrubber.py +149 -0
  16. devtorch_core/audit/service.py +67 -0
  17. devtorch_core/audit/signing.py +127 -0
  18. devtorch_core/broadcast/__init__.py +4 -0
  19. devtorch_core/broadcast/broadcaster.py +100 -0
  20. devtorch_core/broadcast/watcher.py +71 -0
  21. devtorch_core/capability.py +639 -0
  22. devtorch_core/cloud/__init__.py +1 -0
  23. devtorch_core/cloud/client_config.py +472 -0
  24. devtorch_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
  25. devtorch_core/cloud/client_configs/.claude-stdio.json +13 -0
  26. devtorch_core/cloud/client_configs/.cursor-mcp.json +13 -0
  27. devtorch_core/cloud/client_configs/.opencode-bridge.json +13 -0
  28. devtorch_core/cloud/client_configs/.opencode.json +15 -0
  29. devtorch_core/cloud/client_configs/.vscode-mcp.json +13 -0
  30. devtorch_core/cloud/devtorch-mcp-bridge.js +357 -0
  31. devtorch_core/cloud/mcp_client.py +229 -0
  32. devtorch_core/cloud/setup.py +144 -0
  33. devtorch_core/cloud/sync.py +143 -0
  34. devtorch_core/cloud/sync_bundle.py +603 -0
  35. devtorch_core/cloud/sync_conflicts.py +159 -0
  36. devtorch_core/cloud/sync_state.py +159 -0
  37. devtorch_core/cloud/team_sync.py +283 -0
  38. devtorch_core/codex/__init__.py +9 -0
  39. devtorch_core/codex/__main__.py +97 -0
  40. devtorch_core/codex/capture.py +208 -0
  41. devtorch_core/codex/proxy.py +412 -0
  42. devtorch_core/concept_catalog.py +209 -0
  43. devtorch_core/consolidation/__init__.py +3 -0
  44. devtorch_core/consolidation/synthesizer.py +87 -0
  45. devtorch_core/consolidation/workflow.py +175 -0
  46. devtorch_core/daemon/__init__.py +27 -0
  47. devtorch_core/daemon/supervisor.py +293 -0
  48. devtorch_core/daemon/watcher.py +244 -0
  49. devtorch_core/dashboard_api.py +2012 -0
  50. devtorch_core/deltaf.py +97 -0
  51. devtorch_core/disclosure.py +50 -0
  52. devtorch_core/divergence/__init__.py +3 -0
  53. devtorch_core/divergence/detector.py +166 -0
  54. devtorch_core/gateway/__init__.py +32 -0
  55. devtorch_core/gateway/key_manager.py +124 -0
  56. devtorch_core/gateway/metrics_webhook.py +252 -0
  57. devtorch_core/gateway/policy.py +262 -0
  58. devtorch_core/gateway/server.py +727 -0
  59. devtorch_core/gateway/sso.py +233 -0
  60. devtorch_core/gcc.py +1246 -0
  61. devtorch_core/github/__init__.py +35 -0
  62. devtorch_core/github/app.py +240 -0
  63. devtorch_core/github/comment_builder.py +113 -0
  64. devtorch_core/github/pat.py +76 -0
  65. devtorch_core/github/pr_parser.py +82 -0
  66. devtorch_core/github/pr_reporter.py +555 -0
  67. devtorch_core/gitlab/__init__.py +177 -0
  68. devtorch_core/hitl/__init__.py +4 -0
  69. devtorch_core/hitl/channels.py +129 -0
  70. devtorch_core/hitl/orchestrator.py +95 -0
  71. devtorch_core/hooks/__init__.py +17 -0
  72. devtorch_core/hooks/claude_code.py +228 -0
  73. devtorch_core/hooks/git_capture.py +341 -0
  74. devtorch_core/hooks/git_commit.py +182 -0
  75. devtorch_core/hooks/installer.py +733 -0
  76. devtorch_core/hooks/pre_commit.py +157 -0
  77. devtorch_core/hooks/runner.py +344 -0
  78. devtorch_core/identity/__init__.py +4 -0
  79. devtorch_core/identity/agent.py +86 -0
  80. devtorch_core/identity/providers.py +85 -0
  81. devtorch_core/invariants.py +182 -0
  82. devtorch_core/mcp/__init__.py +10 -0
  83. devtorch_core/mcp/auth.py +177 -0
  84. devtorch_core/mcp/server.py +1049 -0
  85. devtorch_core/metrics/__init__.py +35 -0
  86. devtorch_core/metrics/aggregate.py +215 -0
  87. devtorch_core/metrics/calibrate.py +198 -0
  88. devtorch_core/metrics/calibration.py +125 -0
  89. devtorch_core/metrics/credibility.py +288 -0
  90. devtorch_core/metrics/delivery_time.py +70 -0
  91. devtorch_core/metrics/dhs.py +126 -0
  92. devtorch_core/metrics/mcs.py +96 -0
  93. devtorch_core/metrics/roi.py +88 -0
  94. devtorch_core/metrics/session_writer.py +81 -0
  95. devtorch_core/metrics/shadow_ai.py +117 -0
  96. devtorch_core/metrics/sprint_writer.py +243 -0
  97. devtorch_core/observability/__init__.py +78 -0
  98. devtorch_core/observability/datadog.py +157 -0
  99. devtorch_core/observability/formatter.py +119 -0
  100. devtorch_core/observability/report.py +264 -0
  101. devtorch_core/observability/servicenow.py +147 -0
  102. devtorch_core/observability/splunk.py +218 -0
  103. devtorch_core/observability/webhook.py +227 -0
  104. devtorch_core/parser/__init__.py +30 -0
  105. devtorch_core/parser/blocks.py +216 -0
  106. devtorch_core/parser/inference.py +159 -0
  107. devtorch_core/parser/thinking.py +112 -0
  108. devtorch_core/projects.py +169 -0
  109. devtorch_core/prompt_artifact.py +76 -0
  110. devtorch_core/proxy/__init__.py +9 -0
  111. devtorch_core/proxy/routes/__init__.py +1 -0
  112. devtorch_core/proxy/routes/anthropic.py +264 -0
  113. devtorch_core/proxy/routes/azure_openai.py +336 -0
  114. devtorch_core/proxy/routes/gemini.py +331 -0
  115. devtorch_core/proxy/routes/groq.py +284 -0
  116. devtorch_core/proxy/routes/ollama.py +279 -0
  117. devtorch_core/proxy/routes/openai.py +287 -0
  118. devtorch_core/proxy/server.py +356 -0
  119. devtorch_core/query/__init__.py +15 -0
  120. devtorch_core/query/grep.py +181 -0
  121. devtorch_core/query/hybrid.py +86 -0
  122. devtorch_core/query/semantic.py +157 -0
  123. devtorch_core/rdp.py +105 -0
  124. devtorch_core/reasoning/__init__.py +4 -0
  125. devtorch_core/reasoning/entry.py +31 -0
  126. devtorch_core/reasoning/store.py +122 -0
  127. devtorch_core/reasoning_plus/__init__.py +70 -0
  128. devtorch_core/reasoning_plus/augmenter.py +326 -0
  129. devtorch_core/reasoning_plus/capture.py +51 -0
  130. devtorch_core/reasoning_plus/config.py +256 -0
  131. devtorch_core/reasoning_plus/context.py +262 -0
  132. devtorch_core/reasoning_plus/learning/__init__.py +72 -0
  133. devtorch_core/reasoning_plus/learning/analytics.py +141 -0
  134. devtorch_core/reasoning_plus/learning/api.py +313 -0
  135. devtorch_core/reasoning_plus/learning/chain.py +285 -0
  136. devtorch_core/reasoning_plus/learning/composer.py +74 -0
  137. devtorch_core/reasoning_plus/learning/cross_project.py +234 -0
  138. devtorch_core/reasoning_plus/learning/embeddings.py +209 -0
  139. devtorch_core/reasoning_plus/learning/extractor.py +207 -0
  140. devtorch_core/reasoning_plus/learning/models.py +116 -0
  141. devtorch_core/reasoning_plus/learning/provenance.py +126 -0
  142. devtorch_core/reasoning_plus/learning/recorder.py +81 -0
  143. devtorch_core/reasoning_plus/learning/relevance.py +122 -0
  144. devtorch_core/reasoning_plus/learning/state.py +86 -0
  145. devtorch_core/reasoning_plus/learning/store.py +160 -0
  146. devtorch_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
  147. devtorch_core/reasoning_plus/prompt.py +90 -0
  148. devtorch_core/rep.py +134 -0
  149. devtorch_core/rep_network/__init__.py +25 -0
  150. devtorch_core/rep_network/merge.py +70 -0
  151. devtorch_core/rep_network/node.py +137 -0
  152. devtorch_core/rep_network/server.py +140 -0
  153. devtorch_core/rep_network/sync.py +207 -0
  154. devtorch_core/sensitivity.py +182 -0
  155. devtorch_core/serve.py +258 -0
  156. devtorch_core/session/__init__.py +39 -0
  157. devtorch_core/session/disagreement.py +188 -0
  158. devtorch_core/session/models.py +114 -0
  159. devtorch_core/session/orchestrator.py +182 -0
  160. devtorch_core/session/planner.py +169 -0
  161. devtorch_core/session/simulator.py +132 -0
  162. devtorch_core/signing.py +290 -0
  163. devtorch_core/sis.py +197 -0
  164. devtorch_core/storage.py +308 -0
  165. devtorch_core/templates/__init__.py +6 -0
  166. devtorch_core/templates/engine.py +122 -0
  167. devtorch_core/templates/go.py +18 -0
  168. devtorch_core/templates/infra.py +19 -0
  169. devtorch_core/templates/library/__init__.py +18 -0
  170. devtorch_core/templates/library/api_design.md +27 -0
  171. devtorch_core/templates/library/bug_fix.md +27 -0
  172. devtorch_core/templates/library/decision_record.md +27 -0
  173. devtorch_core/templates/library/engine.py +228 -0
  174. devtorch_core/templates/library/security_review.md +30 -0
  175. devtorch_core/templates/python.py +19 -0
  176. devtorch_core/templates/react.py +18 -0
  177. devtorch_core/templates/typescript.py +18 -0
  178. devtorch_core/theta.py +221 -0
  179. devtorch_core/theta_synthesis.py +268 -0
  180. devtorch_core/topics.py +320 -0
  181. devtorch_core/variance.py +219 -0
  182. devtorch_core/wrapper/__init__.py +52 -0
  183. devtorch_core/wrapper/anthropic.py +487 -0
  184. devtorch_core/wrapper/base.py +562 -0
  185. devtorch_core/wrapper/bedrock.py +342 -0
  186. devtorch_core/wrapper/gemini.py +422 -0
  187. devtorch_core/wrapper/ollama.py +527 -0
  188. devtorch_core/wrapper/openai.py +461 -0
  189. devtorch_core-3.0.1.dist-info/METADATA +867 -0
  190. devtorch_core-3.0.1.dist-info/RECORD +193 -0
  191. devtorch_core-3.0.1.dist-info/WHEEL +5 -0
  192. devtorch_core-3.0.1.dist-info/entry_points.txt +2 -0
  193. devtorch_core-3.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,326 @@
1
+ """
2
+ devtorch_core.reasoning_plus.augmenter
3
+ ======================================
4
+ High-level Reasoning Plus orchestrator.
5
+
6
+ - Decides whether to augment based on config and workspace state.
7
+ - Builds the augmented system prompt (RACP + smart context + reasoning directive).
8
+ - Captures inline <thinking> blocks from the response and strips them before returning.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ import logging
14
+ import time
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from devtorch_core.reasoning_plus.capture import (
19
+ commit_reasoning,
20
+ extract_inline_thinking,
21
+ strip_inline_thinking,
22
+ )
23
+ from devtorch_core.reasoning_plus.config import ReasoningPlusConfig, SettingsStore
24
+ from devtorch_core.reasoning_plus.context import gather_smart_context
25
+ from devtorch_core.reasoning_plus.learning import ReasoningPlusLearning
26
+ from devtorch_core.reasoning_plus.prompt import build_reasoning_plus_system_prompt
27
+
28
+ logger = logging.getLogger("devtorch.reasoning_plus")
29
+
30
+
31
+ class ReasoningPlusAugmenter:
32
+ """
33
+ Per-repo augmenter. Caches smart context and config for performance.
34
+
35
+ Usage:
36
+ augmenter = ReasoningPlusAugmenter(repo_path)
37
+ if augmenter.should_augment():
38
+ system_prompt = augmenter.build_system_prompt(base_prompt, user_text)
39
+ clean_text = augmenter.capture_and_strip(raw_text, session_id, user_text)
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ repo_path: Path | str,
45
+ gcc_repo: Any | None = None,
46
+ config: ReasoningPlusConfig | None = None,
47
+ ) -> None:
48
+ self._repo_path = Path(repo_path)
49
+ self._gcc_repo = gcc_repo
50
+
51
+ # If a gcc_repo object is supplied, prefer its gcc_dir for settings.
52
+ gcc_dir = getattr(gcc_repo, "gcc_dir", None)
53
+ self._store = SettingsStore(gcc_dir)
54
+ self._config, self._config_source = (
55
+ (config, "provided") if config is not None else self._store.load()
56
+ )
57
+
58
+ # Learning layer: only if a gcc_dir is available and learning is enabled.
59
+ self._learning: ReasoningPlusLearning | None = None
60
+ if gcc_dir and self._config.learning_enabled:
61
+ try:
62
+ self._learning = ReasoningPlusLearning(
63
+ gcc_dir=gcc_dir,
64
+ repo_path=self._repo_path,
65
+ config=self._config,
66
+ )
67
+ except Exception as exc:
68
+ logger.warning("devtorch: failed to initialize learning layer — %s", exc)
69
+
70
+ # Cache: prompt hash -> (contents, timestamp)
71
+ self._cache: dict[str, tuple[str, float]] = {}
72
+
73
+ # ------------------------------------------------------------------
74
+ # Public API
75
+ # ------------------------------------------------------------------
76
+
77
+ @property
78
+ def config(self) -> ReasoningPlusConfig:
79
+ return self._config
80
+
81
+ @property
82
+ def config_source(self) -> str:
83
+ return self._config_source
84
+
85
+ def should_augment(self) -> bool:
86
+ """Return True if Reasoning Plus should be applied to this call."""
87
+ if not self._config.enabled:
88
+ return False
89
+ # Must be a git repo to gather smart context
90
+ return self._is_git_repo()
91
+
92
+ def build_system_prompt(
93
+ self,
94
+ base_prompt: str,
95
+ prompt_text_for_context: str | None = None,
96
+ *,
97
+ task_prompt: str = "",
98
+ ) -> str:
99
+ """
100
+ Return a Reasoning Plus system prompt.
101
+
102
+ If the augmenter is disabled, returns `base_prompt` unchanged.
103
+ """
104
+ if not self._config.enabled:
105
+ return base_prompt
106
+
107
+ smart_context = ""
108
+ if prompt_text_for_context:
109
+ smart_context = self._get_cached_smart_context(prompt_text_for_context)
110
+
111
+ return build_reasoning_plus_system_prompt(
112
+ base_prompt,
113
+ smart_context=smart_context,
114
+ require_thinking=self._config.require_thinking,
115
+ task_prompt=task_prompt,
116
+ )
117
+
118
+ def capture_and_strip(
119
+ self,
120
+ response_text: str,
121
+ session_id: str,
122
+ prompt_text: str | None = None,
123
+ ) -> str:
124
+ """
125
+ Extract inline reasoning from the response, commit it, and return text
126
+ with the <thinking> blocks stripped.
127
+ """
128
+ if not self._config.enabled:
129
+ return response_text
130
+
131
+ thinking_blocks = extract_inline_thinking(response_text)
132
+ if thinking_blocks:
133
+ combined = "\n\n".join(thinking_blocks)
134
+ try:
135
+ commit_reasoning(
136
+ self._gcc_repo,
137
+ combined,
138
+ session_id,
139
+ prompt_text or "",
140
+ )
141
+ except Exception as exc:
142
+ logger.warning("devtorch: reasoning_plus capture failed — %s", exc)
143
+
144
+ cleaned_text = strip_inline_thinking(response_text)
145
+
146
+ # Record this LLM call in the learning layer so its reasoning can be reused.
147
+ if self._learning and thinking_blocks:
148
+ try:
149
+ call_id = self._learning.record_call(
150
+ call_type="llm",
151
+ reasoning="\n\n".join(thinking_blocks),
152
+ input=prompt_text or "",
153
+ output=cleaned_text,
154
+ outcome="unknown",
155
+ session_id=session_id,
156
+ )
157
+ self._learning.extract_learnings(call_id)
158
+ except Exception as exc:
159
+ logger.warning("devtorch: learning layer recording failed — %s", exc)
160
+
161
+ return cleaned_text
162
+
163
+ def get_learning_context(self, context: str, session_id: str = "") -> str:
164
+ """
165
+ Return a compact <learnings> block for the given context, or "" if none.
166
+
167
+ Callers should append the returned block to the user prompt.
168
+ """
169
+ block, _ = self.get_learning_context_with_provenance(context, session_id=session_id)
170
+ return block
171
+
172
+ def get_learning_context_with_provenance(
173
+ self,
174
+ context: str,
175
+ session_id: str = "",
176
+ call_id: str = "",
177
+ model_name: str = "",
178
+ ) -> tuple[str, list[str]]:
179
+ """
180
+ Return a compact <learnings> block plus the IDs of injected learnings.
181
+
182
+ If *call_id* is provided, a provenance record is saved so outcomes can be
183
+ attributed back to the injected learnings.
184
+ """
185
+ if not self._learning:
186
+ return "", []
187
+ try:
188
+ learnings = self._learning.get_relevant_learnings(
189
+ context,
190
+ top_n=self._config.learning_top_n,
191
+ )
192
+ block = self._learning.compose_user_prompt("", learnings).strip()
193
+ learning_ids = [l.id for l in learnings]
194
+ if call_id and learning_ids:
195
+ self._learning.record_injection(
196
+ learning_ids,
197
+ call_id=call_id,
198
+ session_id=session_id,
199
+ prompt_text=context,
200
+ model_name=model_name,
201
+ )
202
+ return block, learning_ids
203
+ except Exception as exc:
204
+ logger.warning("devtorch: learning context retrieval failed — %s", exc)
205
+ return "", []
206
+
207
+ def build_system_prompt_with_learning_context(
208
+ self,
209
+ base_prompt: str,
210
+ prompt_text_for_context: str | None = None,
211
+ *,
212
+ task_prompt: str = "",
213
+ call_id: str = "",
214
+ session_id: str = "",
215
+ model_name: str = "",
216
+ ) -> tuple[str, list[str]]:
217
+ """
218
+ Build a Reasoning Plus system prompt and append relevant learnings.
219
+
220
+ Returns the augmented prompt and the list of learning IDs that were
221
+ injected (if any). A provenance record is written when *call_id* is set.
222
+ """
223
+ system_prompt = self.build_system_prompt(
224
+ base_prompt,
225
+ prompt_text_for_context=prompt_text_for_context,
226
+ task_prompt=task_prompt,
227
+ )
228
+ learning_ids: list[str] = []
229
+ if prompt_text_for_context:
230
+ block, learning_ids = self.get_learning_context_with_provenance(
231
+ prompt_text_for_context,
232
+ session_id=session_id,
233
+ call_id=call_id,
234
+ model_name=model_name,
235
+ )
236
+ if block:
237
+ system_prompt = f"{system_prompt}\n\n[DevTorch learnings]\n{block}"
238
+ return system_prompt, learning_ids
239
+
240
+ def record_learning_call(
241
+ self,
242
+ *,
243
+ call_type: str,
244
+ reasoning: str,
245
+ session_id: str = "",
246
+ name: str = "",
247
+ input: str = "",
248
+ output: str = "",
249
+ outcome: str = "unknown",
250
+ ) -> str:
251
+ """
252
+ Record a tool/memory/action call in the learning layer.
253
+
254
+ Returns the call ID so the caller can apply feedback later.
255
+ """
256
+ if not self._learning:
257
+ return ""
258
+ try:
259
+ return self._learning.record_call(
260
+ call_type=call_type,
261
+ reasoning=reasoning,
262
+ session_id=session_id,
263
+ name=name,
264
+ input=input,
265
+ output=output,
266
+ outcome=outcome,
267
+ )
268
+ except Exception as exc:
269
+ logger.warning("devtorch: learning call recording failed — %s", exc)
270
+ return ""
271
+
272
+ def invalidate_cache(self) -> None:
273
+ """Clear the smart context cache (call after a devtorch commit)."""
274
+ self._cache.clear()
275
+
276
+ # ------------------------------------------------------------------
277
+ # Private helpers
278
+ # ------------------------------------------------------------------
279
+
280
+ def _get_cached_smart_context(self, prompt_text: str) -> str:
281
+ """Return cached smart context, or gather and cache it."""
282
+ prompt_hash = _hash_prompt(prompt_text)
283
+ now = time.monotonic()
284
+ ttl = self._config.cache_ttl_secs
285
+
286
+ cached = self._cache.get(prompt_hash)
287
+ if cached and (now - cached[1]) < ttl:
288
+ return cached[0]
289
+
290
+ _, contents = gather_smart_context(
291
+ self._repo_path,
292
+ prompt_text,
293
+ top_n=self._config.smart_top_n,
294
+ max_lines=self._config.smart_max_lines,
295
+ max_keywords=self._config.max_keywords,
296
+ )
297
+ self._cache[prompt_hash] = (contents, now)
298
+ return contents
299
+
300
+ def _is_git_repo(self) -> bool:
301
+ try:
302
+ import subprocess
303
+ result = subprocess.run(
304
+ ["git", "rev-parse", "--git-dir"],
305
+ cwd=self._repo_path,
306
+ capture_output=True,
307
+ text=True,
308
+ errors="ignore",
309
+ timeout=5,
310
+ )
311
+ return result.returncode == 0 and result.stdout.strip() != ""
312
+ except Exception:
313
+ return False
314
+
315
+
316
+ def _hash_prompt(prompt_text: str) -> str:
317
+ """Stable hash for cache keys."""
318
+ return hashlib.sha256(prompt_text.encode("utf-8")).hexdigest()[:16]
319
+
320
+
321
+ def is_enabled(repo_path: Path | None = None) -> bool:
322
+ """Convenience helper: is Reasoning Plus enabled for the given repo path?"""
323
+ if not repo_path:
324
+ config, _ = SettingsStore().load()
325
+ return config.enabled
326
+ return ReasoningPlusAugmenter(repo_path).should_augment()
@@ -0,0 +1,51 @@
1
+ """
2
+ devtorch_core.reasoning_plus.capture
3
+ ====================================
4
+ Extract inline <thinking> blocks from LLM output and commit them to .GCC/.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ import re
10
+ from typing import Any
11
+
12
+ logger = logging.getLogger("devtorch.reasoning_plus")
13
+
14
+ _THINKING_RE = re.compile(r"<thinking>(.*?)</thinking>", re.DOTALL | re.IGNORECASE)
15
+
16
+
17
+ def extract_inline_thinking(text: str) -> list[str]:
18
+ """Return all <thinking>...</thinking> bodies found in `text`."""
19
+ return [m.strip() for m in _THINKING_RE.findall(text or "")]
20
+
21
+
22
+ def strip_inline_thinking(text: str) -> str:
23
+ """Remove <thinking>...</thinking> blocks from `text` and collapse whitespace."""
24
+ cleaned = _THINKING_RE.sub("\n", text or "")
25
+ # Collapse multiple blank lines introduced by stripping
26
+ cleaned = re.sub(r"\n{3,}", "\n\n", cleaned)
27
+ return cleaned.strip()
28
+
29
+
30
+ def commit_reasoning(
31
+ repo: Any,
32
+ reasoning_text: str,
33
+ session_id: str,
34
+ prompt_text: str = "",
35
+ ) -> None:
36
+ """
37
+ Commit a reasoning block to .GCC/ via the standard repo.commit() primitive.
38
+
39
+ This keeps the same audit path as all other DevTorch captures.
40
+ """
41
+ try:
42
+ summary = reasoning_text.strip().replace("\n", " ")[:200]
43
+ prompt_summary = prompt_text.strip().replace("\n", " ")[:100]
44
+ message = (
45
+ f"[session={session_id}] reasoning_plus(conf=1.0): {summary}"
46
+ )
47
+ if prompt_summary:
48
+ message += f" | prompt: {prompt_summary}"
49
+ repo.commit(message=message)
50
+ except Exception as exc:
51
+ logger.warning("devtorch: failed to commit reasoning — %s", exc)
@@ -0,0 +1,256 @@
1
+ """
2
+ devtorch_core.reasoning_plus.config
3
+ =================================
4
+ Configuration and settings for Reasoning Plus.
5
+
6
+ Resolution order (highest priority first):
7
+ 1. DEVTORCH_REASONING_PLUS env var (0/1).
8
+ 2. Per-project .GCC/settings.json.
9
+ 3. Global ~/.devtorch/settings.json.
10
+ 4. Built-in defaults (enabled by default).
11
+
12
+ All settings access is defensive: malformed JSON or missing files return
13
+ defaults so that wrappers never fail because of config errors.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import logging
19
+ import os
20
+ from dataclasses import dataclass, field, fields
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ logger = logging.getLogger("devtorch.reasoning_plus")
25
+
26
+ REASONING_PLUS_ENV_VAR = "DEVTORCH_REASONING_PLUS"
27
+ GLOBAL_SETTINGS_DIR = Path.home() / ".devtorch"
28
+ GLOBAL_SETTINGS_PATH = GLOBAL_SETTINGS_DIR / "settings.json"
29
+ PROJECT_SETTINGS_FILENAME = "settings.json"
30
+
31
+
32
+ @dataclass
33
+ class ReasoningPlusConfig:
34
+ """Immutable-ish configuration for Reasoning Plus.
35
+
36
+ Use `with_values()` to create a copy with selected overrides.
37
+ """
38
+
39
+ enabled: bool = True
40
+ smart_top_n: int = 3
41
+ smart_max_lines: int = 150
42
+ context_k: int = 4000
43
+ require_thinking: bool = True
44
+ max_keywords: int = 15
45
+ cache_ttl_secs: int = 60
46
+ learning_enabled: bool = True
47
+ learning_top_n: int = 3
48
+ learning_max_lines: int = 100
49
+ learning_embedding_backend: str = "keyword"
50
+ learning_embedding_model: str | None = None
51
+ learning_extraction_strategy: str = "rule"
52
+ learning_relevance_cache_ttl: int = 60
53
+
54
+ def with_values(self, **kwargs: Any) -> "ReasoningPlusConfig":
55
+ """Return a new config with the provided field overrides."""
56
+ valid = {f.name for f in fields(self)}
57
+ extra = set(kwargs) - valid
58
+ if extra:
59
+ raise ValueError(f"Unknown ReasoningPlusConfig fields: {extra}")
60
+ return ReasoningPlusConfig(
61
+ **{f.name: kwargs.get(f.name, getattr(self, f.name)) for f in fields(self)}
62
+ )
63
+
64
+ def to_dict(self) -> dict[str, Any]:
65
+ return {
66
+ "enabled": self.enabled,
67
+ "smart_top_n": self.smart_top_n,
68
+ "smart_max_lines": self.smart_max_lines,
69
+ "context_k": self.context_k,
70
+ "require_thinking": self.require_thinking,
71
+ "max_keywords": self.max_keywords,
72
+ "cache_ttl_secs": self.cache_ttl_secs,
73
+ "learning_enabled": self.learning_enabled,
74
+ "learning_top_n": self.learning_top_n,
75
+ "learning_max_lines": self.learning_max_lines,
76
+ "learning_embedding_backend": self.learning_embedding_backend,
77
+ "learning_embedding_model": self.learning_embedding_model,
78
+ "learning_extraction_strategy": self.learning_extraction_strategy,
79
+ "learning_relevance_cache_ttl": self.learning_relevance_cache_ttl,
80
+ }
81
+
82
+ @classmethod
83
+ def from_dict(cls, data: dict[str, Any]) -> "ReasoningPlusConfig":
84
+ """Build from a dict, ignoring unknown keys and using defaults for missing ones."""
85
+ return cls(
86
+ enabled=_bool(data.get("enabled", True)),
87
+ smart_top_n=_int(data.get("smart_top_n", 3), 3),
88
+ smart_max_lines=_int(data.get("smart_max_lines", 150), 150),
89
+ context_k=_int(data.get("context_k", 4000), 4000),
90
+ require_thinking=_bool(data.get("require_thinking", True)),
91
+ max_keywords=_int(data.get("max_keywords", 15), 15),
92
+ cache_ttl_secs=_int(data.get("cache_ttl_secs", 60), 60),
93
+ learning_enabled=_bool(data.get("learning_enabled", True)),
94
+ learning_top_n=_int(data.get("learning_top_n", 3), 3),
95
+ learning_max_lines=_int(data.get("learning_max_lines", 100), 100),
96
+ learning_embedding_backend=_str(data.get("learning_embedding_backend", "keyword"), "keyword"),
97
+ learning_embedding_model=_optional_str(data.get("learning_embedding_model")),
98
+ learning_extraction_strategy=_str(data.get("learning_extraction_strategy", "rule"), "rule"),
99
+ learning_relevance_cache_ttl=_int(data.get("learning_relevance_cache_ttl", 60), 60),
100
+ )
101
+
102
+
103
+ class SettingsStore:
104
+ """Read and write Reasoning Plus settings at project and global levels."""
105
+
106
+ def __init__(self, gcc_dir: Path | None = None) -> None:
107
+ self._project_dir = gcc_dir
108
+ self._global_path = GLOBAL_SETTINGS_PATH
109
+
110
+ # ------------------------------------------------------------------
111
+ # Read
112
+ # ------------------------------------------------------------------
113
+
114
+ def load(self) -> tuple[ReasoningPlusConfig, str]:
115
+ """Return (config, source) where source describes where the config came from."""
116
+ # 1. Env var (highest priority)
117
+ env_val = os.environ.get(REASONING_PLUS_ENV_VAR, "").strip()
118
+ if env_val:
119
+ enabled = env_val in ("1", "true", "yes", "on")
120
+ config = ReasoningPlusConfig(enabled=enabled)
121
+ return config, "environment"
122
+
123
+ # 2. Project settings
124
+ project = self._read_project_settings()
125
+ if project is not None:
126
+ return ReasoningPlusConfig.from_dict(project.get("reasoning_plus", {})), "project"
127
+
128
+ # 3. Global settings
129
+ global_ = self._read_global_settings()
130
+ if global_ is not None:
131
+ return ReasoningPlusConfig.from_dict(global_.get("reasoning_plus", {})), "global"
132
+
133
+ # 4. Defaults
134
+ return ReasoningPlusConfig(), "default"
135
+
136
+ def _read_project_settings(self) -> dict[str, Any] | None:
137
+ if not self._project_dir:
138
+ return None
139
+ path = self._project_dir / PROJECT_SETTINGS_FILENAME
140
+ return _read_json(path)
141
+
142
+ def _read_global_settings(self) -> dict[str, Any] | None:
143
+ return _read_json(self._global_path)
144
+
145
+ # ------------------------------------------------------------------
146
+ # Write
147
+ # ------------------------------------------------------------------
148
+
149
+ def save_project(self, config: ReasoningPlusConfig) -> None:
150
+ """Write the Reasoning Plus section to .GCC/settings.json (creates file if needed)."""
151
+ if not self._project_dir:
152
+ raise ValueError("No project directory available for saving settings")
153
+ self._project_dir.mkdir(parents=True, exist_ok=True)
154
+ path = self._project_dir / PROJECT_SETTINGS_FILENAME
155
+ settings = _read_json(path) or {}
156
+ settings["reasoning_plus"] = config.to_dict()
157
+ settings.setdefault("version", 1)
158
+ _write_json(path, settings)
159
+
160
+ def save_global(self, config: ReasoningPlusConfig) -> None:
161
+ """Write the Reasoning Plus section to ~/.devtorch/settings.json."""
162
+ GLOBAL_SETTINGS_DIR.mkdir(parents=True, exist_ok=True)
163
+ settings = _read_json(self._global_path) or {}
164
+ settings["reasoning_plus"] = config.to_dict()
165
+ settings.setdefault("version", 1)
166
+ _write_json(self._global_path, settings)
167
+
168
+ def reset_project(self) -> None:
169
+ """Remove the Reasoning Plus section from project settings.
170
+
171
+ If the settings file is empty after removal (only version remains), delete it.
172
+ """
173
+ if not self._project_dir:
174
+ return
175
+ path = self._project_dir / PROJECT_SETTINGS_FILENAME
176
+ settings = _read_json(path)
177
+ if settings and "reasoning_plus" in settings:
178
+ del settings["reasoning_plus"]
179
+ remaining_keys = [k for k in settings.keys() if k != "version"]
180
+ if remaining_keys:
181
+ _write_json(path, settings)
182
+ else:
183
+ try:
184
+ path.unlink()
185
+ except Exception:
186
+ pass
187
+
188
+
189
+ # ---------------------------------------------------------------------------
190
+ # Helpers
191
+ # ---------------------------------------------------------------------------
192
+
193
+ def _bool(value: Any) -> bool:
194
+ if isinstance(value, bool):
195
+ return value
196
+ if isinstance(value, str):
197
+ return value.strip().lower() in ("1", "true", "yes", "on")
198
+ return bool(value)
199
+
200
+
201
+ def _int(value: Any, default: int) -> int:
202
+ try:
203
+ return int(value)
204
+ except (TypeError, ValueError):
205
+ return default
206
+
207
+
208
+ def _str(value: Any, default: str) -> str:
209
+ if isinstance(value, str):
210
+ return value.strip()
211
+ return default
212
+
213
+
214
+ def _optional_str(value: Any) -> str | None:
215
+ if value is None:
216
+ return None
217
+ if isinstance(value, str):
218
+ value = value.strip()
219
+ return value if value else None
220
+ return None
221
+
222
+
223
+ def _read_json(path: Path) -> dict[str, Any] | None:
224
+ if not path.exists():
225
+ return None
226
+ try:
227
+ with open(path, "r", encoding="utf-8") as f:
228
+ data = json.load(f)
229
+ if isinstance(data, dict):
230
+ return data
231
+ except Exception as exc:
232
+ logger.warning("devtorch: failed to read settings from %s — %s", path, exc)
233
+ return None
234
+
235
+
236
+ def _write_json(path: Path, data: dict[str, Any]) -> None:
237
+ """Atomic JSON write (temp file + rename)."""
238
+ tmp = path.with_suffix(path.suffix + ".tmp")
239
+ try:
240
+ with open(tmp, "w", encoding="utf-8") as f:
241
+ json.dump(data, f, indent=2)
242
+ tmp.replace(path)
243
+ except Exception as exc:
244
+ logger.warning("devtorch: failed to write settings to %s — %s", path, exc)
245
+ raise
246
+ finally:
247
+ if tmp.exists():
248
+ try:
249
+ tmp.unlink()
250
+ except Exception:
251
+ pass
252
+
253
+
254
+ def load_config(gcc_dir: Path | None = None) -> tuple[ReasoningPlusConfig, str]:
255
+ """Convenience function: load config + source."""
256
+ return SettingsStore(gcc_dir).load()