thinkstack-core 4.0.0__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 (205) hide show
  1. thinkstack_core/__init__.py +158 -0
  2. thinkstack_core/aggphi_textual.py +275 -0
  3. thinkstack_core/alerts/__init__.py +23 -0
  4. thinkstack_core/alerts/base.py +46 -0
  5. thinkstack_core/alerts/config.py +60 -0
  6. thinkstack_core/alerts/dispatcher.py +110 -0
  7. thinkstack_core/alerts/jira.py +96 -0
  8. thinkstack_core/alerts/linear.py +72 -0
  9. thinkstack_core/alerts/pagerduty.py +66 -0
  10. thinkstack_core/alerts/slack.py +81 -0
  11. thinkstack_core/alerts/teams.py +70 -0
  12. thinkstack_core/audit/__init__.py +43 -0
  13. thinkstack_core/audit/exporter.py +297 -0
  14. thinkstack_core/audit/privacy.py +101 -0
  15. thinkstack_core/audit/scrubber.py +149 -0
  16. thinkstack_core/audit/service.py +67 -0
  17. thinkstack_core/audit/signing.py +127 -0
  18. thinkstack_core/broadcast/__init__.py +4 -0
  19. thinkstack_core/broadcast/broadcaster.py +100 -0
  20. thinkstack_core/broadcast/watcher.py +71 -0
  21. thinkstack_core/capability.py +639 -0
  22. thinkstack_core/cloud/__init__.py +1 -0
  23. thinkstack_core/cloud/client_config.py +472 -0
  24. thinkstack_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
  25. thinkstack_core/cloud/client_configs/.claude-stdio.json +13 -0
  26. thinkstack_core/cloud/client_configs/.cursor-mcp.json +13 -0
  27. thinkstack_core/cloud/client_configs/.opencode-bridge.json +13 -0
  28. thinkstack_core/cloud/client_configs/.opencode.json +15 -0
  29. thinkstack_core/cloud/client_configs/.vscode-mcp.json +13 -0
  30. thinkstack_core/cloud/mcp_client.py +229 -0
  31. thinkstack_core/cloud/setup.py +144 -0
  32. thinkstack_core/cloud/sync.py +143 -0
  33. thinkstack_core/cloud/sync_bundle.py +639 -0
  34. thinkstack_core/cloud/sync_conflicts.py +183 -0
  35. thinkstack_core/cloud/sync_state.py +159 -0
  36. thinkstack_core/cloud/team_sync.py +337 -0
  37. thinkstack_core/cloud/thinkstack-mcp-bridge.js +357 -0
  38. thinkstack_core/codex/__init__.py +9 -0
  39. thinkstack_core/codex/__main__.py +97 -0
  40. thinkstack_core/codex/capture.py +208 -0
  41. thinkstack_core/codex/proxy.py +412 -0
  42. thinkstack_core/compat.py +103 -0
  43. thinkstack_core/concept_catalog.py +209 -0
  44. thinkstack_core/consolidation/__init__.py +3 -0
  45. thinkstack_core/consolidation/synthesizer.py +87 -0
  46. thinkstack_core/consolidation/workflow.py +175 -0
  47. thinkstack_core/daemon/__init__.py +27 -0
  48. thinkstack_core/daemon/supervisor.py +293 -0
  49. thinkstack_core/daemon/watcher.py +244 -0
  50. thinkstack_core/dashboard_api.py +2012 -0
  51. thinkstack_core/deltaf.py +97 -0
  52. thinkstack_core/disclosure.py +50 -0
  53. thinkstack_core/divergence/__init__.py +3 -0
  54. thinkstack_core/divergence/detector.py +166 -0
  55. thinkstack_core/gateway/__init__.py +32 -0
  56. thinkstack_core/gateway/key_manager.py +124 -0
  57. thinkstack_core/gateway/metrics_webhook.py +252 -0
  58. thinkstack_core/gateway/policy.py +262 -0
  59. thinkstack_core/gateway/server.py +727 -0
  60. thinkstack_core/gateway/sso.py +233 -0
  61. thinkstack_core/gcc.py +1246 -0
  62. thinkstack_core/github/__init__.py +35 -0
  63. thinkstack_core/github/app.py +240 -0
  64. thinkstack_core/github/comment_builder.py +113 -0
  65. thinkstack_core/github/pat.py +76 -0
  66. thinkstack_core/github/pr_parser.py +82 -0
  67. thinkstack_core/github/pr_reporter.py +555 -0
  68. thinkstack_core/gitlab/__init__.py +177 -0
  69. thinkstack_core/hitl/__init__.py +4 -0
  70. thinkstack_core/hitl/channels.py +129 -0
  71. thinkstack_core/hitl/orchestrator.py +95 -0
  72. thinkstack_core/hooks/__init__.py +17 -0
  73. thinkstack_core/hooks/claude_code.py +228 -0
  74. thinkstack_core/hooks/git_capture.py +341 -0
  75. thinkstack_core/hooks/git_commit.py +182 -0
  76. thinkstack_core/hooks/installer.py +850 -0
  77. thinkstack_core/hooks/pre_commit.py +157 -0
  78. thinkstack_core/hooks/runner.py +386 -0
  79. thinkstack_core/identity/__init__.py +4 -0
  80. thinkstack_core/identity/agent.py +86 -0
  81. thinkstack_core/identity/providers.py +85 -0
  82. thinkstack_core/invariants.py +182 -0
  83. thinkstack_core/mcp/__init__.py +10 -0
  84. thinkstack_core/mcp/auth.py +177 -0
  85. thinkstack_core/mcp/server.py +1215 -0
  86. thinkstack_core/metrics/__init__.py +35 -0
  87. thinkstack_core/metrics/aggregate.py +215 -0
  88. thinkstack_core/metrics/calibrate.py +198 -0
  89. thinkstack_core/metrics/calibration.py +125 -0
  90. thinkstack_core/metrics/credibility.py +288 -0
  91. thinkstack_core/metrics/delivery_time.py +70 -0
  92. thinkstack_core/metrics/dhs.py +126 -0
  93. thinkstack_core/metrics/mcs.py +96 -0
  94. thinkstack_core/metrics/roi.py +88 -0
  95. thinkstack_core/metrics/session_writer.py +81 -0
  96. thinkstack_core/metrics/shadow_ai.py +117 -0
  97. thinkstack_core/metrics/sprint_writer.py +243 -0
  98. thinkstack_core/observability/__init__.py +78 -0
  99. thinkstack_core/observability/datadog.py +157 -0
  100. thinkstack_core/observability/formatter.py +119 -0
  101. thinkstack_core/observability/report.py +264 -0
  102. thinkstack_core/observability/servicenow.py +147 -0
  103. thinkstack_core/observability/splunk.py +218 -0
  104. thinkstack_core/observability/webhook.py +227 -0
  105. thinkstack_core/parser/__init__.py +30 -0
  106. thinkstack_core/parser/blocks.py +216 -0
  107. thinkstack_core/parser/inference.py +159 -0
  108. thinkstack_core/parser/thinking.py +112 -0
  109. thinkstack_core/projects.py +169 -0
  110. thinkstack_core/prompt_artifact.py +76 -0
  111. thinkstack_core/proxy/__init__.py +9 -0
  112. thinkstack_core/proxy/routes/__init__.py +1 -0
  113. thinkstack_core/proxy/routes/anthropic.py +264 -0
  114. thinkstack_core/proxy/routes/azure_openai.py +336 -0
  115. thinkstack_core/proxy/routes/gemini.py +331 -0
  116. thinkstack_core/proxy/routes/groq.py +284 -0
  117. thinkstack_core/proxy/routes/ollama.py +279 -0
  118. thinkstack_core/proxy/routes/openai.py +287 -0
  119. thinkstack_core/proxy/server.py +356 -0
  120. thinkstack_core/query/__init__.py +15 -0
  121. thinkstack_core/query/grep.py +181 -0
  122. thinkstack_core/query/hybrid.py +86 -0
  123. thinkstack_core/query/semantic.py +157 -0
  124. thinkstack_core/rdp.py +105 -0
  125. thinkstack_core/reasoning/__init__.py +4 -0
  126. thinkstack_core/reasoning/entry.py +31 -0
  127. thinkstack_core/reasoning/store.py +122 -0
  128. thinkstack_core/reasoning_plus/__init__.py +70 -0
  129. thinkstack_core/reasoning_plus/augmenter.py +337 -0
  130. thinkstack_core/reasoning_plus/capture.py +51 -0
  131. thinkstack_core/reasoning_plus/config.py +313 -0
  132. thinkstack_core/reasoning_plus/context.py +262 -0
  133. thinkstack_core/reasoning_plus/learning/__init__.py +125 -0
  134. thinkstack_core/reasoning_plus/learning/analytics.py +141 -0
  135. thinkstack_core/reasoning_plus/learning/api.py +784 -0
  136. thinkstack_core/reasoning_plus/learning/chain.py +285 -0
  137. thinkstack_core/reasoning_plus/learning/composer.py +141 -0
  138. thinkstack_core/reasoning_plus/learning/conflicts.py +184 -0
  139. thinkstack_core/reasoning_plus/learning/context_collector.py +194 -0
  140. thinkstack_core/reasoning_plus/learning/cross_project.py +234 -0
  141. thinkstack_core/reasoning_plus/learning/deny_list.py +108 -0
  142. thinkstack_core/reasoning_plus/learning/embeddings.py +209 -0
  143. thinkstack_core/reasoning_plus/learning/evolution.py +119 -0
  144. thinkstack_core/reasoning_plus/learning/extractor.py +271 -0
  145. thinkstack_core/reasoning_plus/learning/filter_five_layer.py +95 -0
  146. thinkstack_core/reasoning_plus/learning/models.py +149 -0
  147. thinkstack_core/reasoning_plus/learning/org_store.py +156 -0
  148. thinkstack_core/reasoning_plus/learning/pii.py +142 -0
  149. thinkstack_core/reasoning_plus/learning/promotion.py +58 -0
  150. thinkstack_core/reasoning_plus/learning/provenance.py +126 -0
  151. thinkstack_core/reasoning_plus/learning/recorder.py +81 -0
  152. thinkstack_core/reasoning_plus/learning/relevance.py +122 -0
  153. thinkstack_core/reasoning_plus/learning/state.py +86 -0
  154. thinkstack_core/reasoning_plus/learning/store.py +178 -0
  155. thinkstack_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
  156. thinkstack_core/reasoning_plus/prompt.py +90 -0
  157. thinkstack_core/rep.py +134 -0
  158. thinkstack_core/rep_network/__init__.py +25 -0
  159. thinkstack_core/rep_network/merge.py +70 -0
  160. thinkstack_core/rep_network/node.py +137 -0
  161. thinkstack_core/rep_network/server.py +140 -0
  162. thinkstack_core/rep_network/sync.py +207 -0
  163. thinkstack_core/sensitivity.py +182 -0
  164. thinkstack_core/serve.py +258 -0
  165. thinkstack_core/session/__init__.py +39 -0
  166. thinkstack_core/session/disagreement.py +188 -0
  167. thinkstack_core/session/models.py +114 -0
  168. thinkstack_core/session/orchestrator.py +182 -0
  169. thinkstack_core/session/planner.py +169 -0
  170. thinkstack_core/session/simulator.py +132 -0
  171. thinkstack_core/signing.py +290 -0
  172. thinkstack_core/sis.py +197 -0
  173. thinkstack_core/skills/pr-reviewer/SKILL.md +204 -0
  174. thinkstack_core/skills/thinkstack-auto-sync/SKILL.md +175 -0
  175. thinkstack_core/skills/thinkstack-session-start/SKILL.md +136 -0
  176. thinkstack_core/storage.py +308 -0
  177. thinkstack_core/templates/__init__.py +6 -0
  178. thinkstack_core/templates/engine.py +122 -0
  179. thinkstack_core/templates/go.py +18 -0
  180. thinkstack_core/templates/infra.py +19 -0
  181. thinkstack_core/templates/library/__init__.py +18 -0
  182. thinkstack_core/templates/library/api_design.md +27 -0
  183. thinkstack_core/templates/library/bug_fix.md +27 -0
  184. thinkstack_core/templates/library/decision_record.md +27 -0
  185. thinkstack_core/templates/library/engine.py +228 -0
  186. thinkstack_core/templates/library/security_review.md +30 -0
  187. thinkstack_core/templates/python.py +19 -0
  188. thinkstack_core/templates/react.py +18 -0
  189. thinkstack_core/templates/typescript.py +18 -0
  190. thinkstack_core/theta.py +221 -0
  191. thinkstack_core/theta_synthesis.py +268 -0
  192. thinkstack_core/topics.py +320 -0
  193. thinkstack_core/variance.py +219 -0
  194. thinkstack_core/wrapper/__init__.py +52 -0
  195. thinkstack_core/wrapper/anthropic.py +487 -0
  196. thinkstack_core/wrapper/base.py +562 -0
  197. thinkstack_core/wrapper/bedrock.py +342 -0
  198. thinkstack_core/wrapper/gemini.py +422 -0
  199. thinkstack_core/wrapper/ollama.py +527 -0
  200. thinkstack_core/wrapper/openai.py +461 -0
  201. thinkstack_core-4.0.0.dist-info/METADATA +868 -0
  202. thinkstack_core-4.0.0.dist-info/RECORD +205 -0
  203. thinkstack_core-4.0.0.dist-info/WHEEL +5 -0
  204. thinkstack_core-4.0.0.dist-info/entry_points.txt +2 -0
  205. thinkstack_core-4.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,337 @@
1
+ """
2
+ thinkstack_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 thinkstack_core.reasoning_plus.capture import (
19
+ commit_reasoning,
20
+ extract_inline_thinking,
21
+ strip_inline_thinking,
22
+ )
23
+ from thinkstack_core.reasoning_plus.config import ReasoningPlusConfig, SettingsStore
24
+ from thinkstack_core.reasoning_plus.context import gather_smart_context
25
+ from thinkstack_core.reasoning_plus.learning import ReasoningPlusLearning
26
+ from thinkstack_core.reasoning_plus.prompt import build_reasoning_plus_system_prompt
27
+
28
+ logger = logging.getLogger("thinkstack.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("thinkstack: 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("thinkstack: 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.queue_extraction(call_id)
158
+ except Exception as exc:
159
+ logger.warning("thinkstack: 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("thinkstack: 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[ThinkStack 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("thinkstack: learning call recording failed — %s", exc)
270
+ return ""
271
+
272
+ def flush_learnings(self) -> int:
273
+ """Flush any pending batch extractions. Returns number of learning IDs extracted."""
274
+ if not self._learning:
275
+ return 0
276
+ try:
277
+ ids = self._learning.flush_extractions()
278
+ return len(ids)
279
+ except Exception as exc:
280
+ logger.warning("thinkstack: learning flush failed — %s", exc)
281
+ return 0
282
+
283
+ def invalidate_cache(self) -> None:
284
+ """Clear the smart context cache (call after a thinkstack commit)."""
285
+ self._cache.clear()
286
+
287
+ # ------------------------------------------------------------------
288
+ # Private helpers
289
+ # ------------------------------------------------------------------
290
+
291
+ def _get_cached_smart_context(self, prompt_text: str) -> str:
292
+ """Return cached smart context, or gather and cache it."""
293
+ prompt_hash = _hash_prompt(prompt_text)
294
+ now = time.monotonic()
295
+ ttl = self._config.cache_ttl_secs
296
+
297
+ cached = self._cache.get(prompt_hash)
298
+ if cached and (now - cached[1]) < ttl:
299
+ return cached[0]
300
+
301
+ _, contents = gather_smart_context(
302
+ self._repo_path,
303
+ prompt_text,
304
+ top_n=self._config.smart_top_n,
305
+ max_lines=self._config.smart_max_lines,
306
+ max_keywords=self._config.max_keywords,
307
+ )
308
+ self._cache[prompt_hash] = (contents, now)
309
+ return contents
310
+
311
+ def _is_git_repo(self) -> bool:
312
+ try:
313
+ import subprocess
314
+ result = subprocess.run(
315
+ ["git", "rev-parse", "--git-dir"],
316
+ cwd=self._repo_path,
317
+ capture_output=True,
318
+ text=True,
319
+ errors="ignore",
320
+ timeout=5,
321
+ )
322
+ return result.returncode == 0 and result.stdout.strip() != ""
323
+ except Exception:
324
+ return False
325
+
326
+
327
+ def _hash_prompt(prompt_text: str) -> str:
328
+ """Stable hash for cache keys."""
329
+ return hashlib.sha256(prompt_text.encode("utf-8")).hexdigest()[:16]
330
+
331
+
332
+ def is_enabled(repo_path: Path | None = None) -> bool:
333
+ """Convenience helper: is Reasoning Plus enabled for the given repo path?"""
334
+ if not repo_path:
335
+ config, _ = SettingsStore().load()
336
+ return config.enabled
337
+ return ReasoningPlusAugmenter(repo_path).should_augment()
@@ -0,0 +1,51 @@
1
+ """
2
+ thinkstack_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("thinkstack.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 ThinkStack 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("thinkstack: failed to commit reasoning — %s", exc)
@@ -0,0 +1,313 @@
1
+ """
2
+ thinkstack_core.reasoning_plus.config
3
+ =================================
4
+ Configuration and settings for Reasoning Plus.
5
+
6
+ Resolution order (highest priority first):
7
+ 1. THINKSTACK_REASONING_PLUS env var (0/1).
8
+ 2. Per-project .GCC/settings.json.
9
+ 3. Global ~/.thinkstack/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("thinkstack.reasoning_plus")
25
+
26
+ REASONING_PLUS_ENV_VAR = "THINKSTACK_REASONING_PLUS"
27
+ GLOBAL_SETTINGS_DIR = Path.home() / ".thinkstack"
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_extraction_model: str = "claude-3-haiku-20240307"
53
+ learning_relevance_cache_ttl: int = 60
54
+ learning_batch_threshold: int = 5
55
+ learning_token_budget: int = 500
56
+ learning_relevance_threshold: float = 0.4
57
+ disclosure_policy: str = "standard"
58
+ learning_cross_project_enabled: bool = True
59
+ learning_org_sync_enabled: bool = True
60
+ learning_cross_project_top_n: int = 3
61
+ learning_max_org_learnings_per_call: int = 5
62
+ auto_sync_enabled: bool = True
63
+ auto_sync_interval_secs: int = 300
64
+ auto_sync_on_tool_use: bool = True
65
+ auto_sync_on_startup: bool = True
66
+ auto_sync_on_stop: bool = True
67
+
68
+ def with_values(self, **kwargs: Any) -> "ReasoningPlusConfig":
69
+ """Return a new config with the provided field overrides."""
70
+ valid = {f.name for f in fields(self)}
71
+ extra = set(kwargs) - valid
72
+ if extra:
73
+ raise ValueError(f"Unknown ReasoningPlusConfig fields: {extra}")
74
+ return ReasoningPlusConfig(
75
+ **{f.name: kwargs.get(f.name, getattr(self, f.name)) for f in fields(self)}
76
+ )
77
+
78
+ def to_dict(self) -> dict[str, Any]:
79
+ return {
80
+ "enabled": self.enabled,
81
+ "smart_top_n": self.smart_top_n,
82
+ "smart_max_lines": self.smart_max_lines,
83
+ "context_k": self.context_k,
84
+ "require_thinking": self.require_thinking,
85
+ "max_keywords": self.max_keywords,
86
+ "cache_ttl_secs": self.cache_ttl_secs,
87
+ "learning_enabled": self.learning_enabled,
88
+ "learning_top_n": self.learning_top_n,
89
+ "learning_max_lines": self.learning_max_lines,
90
+ "learning_embedding_backend": self.learning_embedding_backend,
91
+ "learning_embedding_model": self.learning_embedding_model,
92
+ "learning_extraction_strategy": self.learning_extraction_strategy,
93
+ "learning_extraction_model": self.learning_extraction_model,
94
+ "learning_relevance_cache_ttl": self.learning_relevance_cache_ttl,
95
+ "learning_batch_threshold": self.learning_batch_threshold,
96
+ "learning_token_budget": self.learning_token_budget,
97
+ "learning_relevance_threshold": self.learning_relevance_threshold,
98
+ "disclosure_policy": self.disclosure_policy,
99
+ "learning_cross_project_enabled": self.learning_cross_project_enabled,
100
+ "learning_org_sync_enabled": self.learning_org_sync_enabled,
101
+ "learning_cross_project_top_n": self.learning_cross_project_top_n,
102
+ "learning_max_org_learnings_per_call": self.learning_max_org_learnings_per_call,
103
+ "auto_sync_enabled": self.auto_sync_enabled,
104
+ "auto_sync_interval_secs": self.auto_sync_interval_secs,
105
+ "auto_sync_on_tool_use": self.auto_sync_on_tool_use,
106
+ "auto_sync_on_startup": self.auto_sync_on_startup,
107
+ "auto_sync_on_stop": self.auto_sync_on_stop,
108
+ }
109
+
110
+ @classmethod
111
+ def _validate_disclosure_policy(cls, value: str, default: str = "standard") -> str:
112
+ _VALID = frozenset({"strict", "standard", "permissive"})
113
+ if value in _VALID:
114
+ return value
115
+ logger.warning("thinkstack: invalid disclosure_policy '%s' — falling back to '%s'", value, default)
116
+ return default
117
+
118
+ @classmethod
119
+ def from_dict(cls, data: dict[str, Any]) -> "ReasoningPlusConfig":
120
+ """Build from a dict, ignoring unknown keys and using defaults for missing ones."""
121
+ return cls(
122
+ enabled=_bool(data.get("enabled", True)),
123
+ smart_top_n=_int(data.get("smart_top_n", 3), 3),
124
+ smart_max_lines=_int(data.get("smart_max_lines", 150), 150),
125
+ context_k=_int(data.get("context_k", 4000), 4000),
126
+ require_thinking=_bool(data.get("require_thinking", True)),
127
+ max_keywords=_int(data.get("max_keywords", 15), 15),
128
+ cache_ttl_secs=_int(data.get("cache_ttl_secs", 60), 60),
129
+ learning_enabled=_bool(data.get("learning_enabled", True)),
130
+ learning_top_n=_int(data.get("learning_top_n", 3), 3),
131
+ learning_max_lines=_int(data.get("learning_max_lines", 100), 100),
132
+ learning_embedding_backend=_str(data.get("learning_embedding_backend", "keyword"), "keyword"),
133
+ learning_embedding_model=_optional_str(data.get("learning_embedding_model")),
134
+ learning_extraction_strategy=_str(data.get("learning_extraction_strategy", "rule"), "rule"),
135
+ learning_extraction_model=_str(data.get("learning_extraction_model", "claude-3-haiku-20240307"), "claude-3-haiku-20240307"),
136
+ learning_relevance_cache_ttl=_int(data.get("learning_relevance_cache_ttl", 60), 60),
137
+ learning_batch_threshold=_int(data.get("learning_batch_threshold", 5), 5),
138
+ learning_token_budget=_int(data.get("learning_token_budget", 500), 500),
139
+ learning_relevance_threshold=_float(data.get("learning_relevance_threshold", 0.4), 0.4),
140
+ disclosure_policy=cls._validate_disclosure_policy(_str(data.get("disclosure_policy", "standard"), "standard")),
141
+ learning_cross_project_enabled=_bool(data.get("learning_cross_project_enabled", True)),
142
+ learning_org_sync_enabled=_bool(data.get("learning_org_sync_enabled", True)),
143
+ learning_cross_project_top_n=_int(data.get("learning_cross_project_top_n", 3), 3),
144
+ learning_max_org_learnings_per_call=_int(data.get("learning_max_org_learnings_per_call", 5), 5),
145
+ auto_sync_enabled=_bool(data.get("auto_sync_enabled", True)),
146
+ auto_sync_interval_secs=_int(data.get("auto_sync_interval_secs", 300), 300),
147
+ auto_sync_on_tool_use=_bool(data.get("auto_sync_on_tool_use", True)),
148
+ auto_sync_on_startup=_bool(data.get("auto_sync_on_startup", True)),
149
+ auto_sync_on_stop=_bool(data.get("auto_sync_on_stop", True)),
150
+ )
151
+
152
+
153
+ class SettingsStore:
154
+ """Read and write Reasoning Plus settings at project and global levels."""
155
+
156
+ def __init__(self, gcc_dir: Path | None = None) -> None:
157
+ self._project_dir = gcc_dir
158
+ self._global_path = GLOBAL_SETTINGS_PATH
159
+
160
+ # ------------------------------------------------------------------
161
+ # Read
162
+ # ------------------------------------------------------------------
163
+
164
+ def load(self) -> tuple[ReasoningPlusConfig, str]:
165
+ """Return (config, source) where source describes where the config came from."""
166
+ # 1. Env var (highest priority)
167
+ env_val = os.environ.get(REASONING_PLUS_ENV_VAR, "").strip()
168
+ if env_val:
169
+ enabled = env_val in ("1", "true", "yes", "on")
170
+ config = ReasoningPlusConfig(enabled=enabled)
171
+ return config, "environment"
172
+
173
+ # 2. Project settings
174
+ project = self._read_project_settings()
175
+ if project is not None:
176
+ return ReasoningPlusConfig.from_dict(project.get("reasoning_plus", {})), "project"
177
+
178
+ # 3. Global settings
179
+ global_ = self._read_global_settings()
180
+ if global_ is not None:
181
+ return ReasoningPlusConfig.from_dict(global_.get("reasoning_plus", {})), "global"
182
+
183
+ # 4. Defaults
184
+ return ReasoningPlusConfig(), "default"
185
+
186
+ def _read_project_settings(self) -> dict[str, Any] | None:
187
+ if not self._project_dir:
188
+ return None
189
+ path = self._project_dir / PROJECT_SETTINGS_FILENAME
190
+ return _read_json(path)
191
+
192
+ def _read_global_settings(self) -> dict[str, Any] | None:
193
+ return _read_json(self._global_path)
194
+
195
+ # ------------------------------------------------------------------
196
+ # Write
197
+ # ------------------------------------------------------------------
198
+
199
+ def save_project(self, config: ReasoningPlusConfig) -> None:
200
+ """Write the Reasoning Plus section to .GCC/settings.json (creates file if needed)."""
201
+ if not self._project_dir:
202
+ raise ValueError("No project directory available for saving settings")
203
+ self._project_dir.mkdir(parents=True, exist_ok=True)
204
+ path = self._project_dir / PROJECT_SETTINGS_FILENAME
205
+ settings = _read_json(path) or {}
206
+ settings["reasoning_plus"] = config.to_dict()
207
+ settings.setdefault("version", 1)
208
+ _write_json(path, settings)
209
+
210
+ def save_global(self, config: ReasoningPlusConfig) -> None:
211
+ """Write the Reasoning Plus section to ~/.thinkstack/settings.json."""
212
+ GLOBAL_SETTINGS_DIR.mkdir(parents=True, exist_ok=True)
213
+ settings = _read_json(self._global_path) or {}
214
+ settings["reasoning_plus"] = config.to_dict()
215
+ settings.setdefault("version", 1)
216
+ _write_json(self._global_path, settings)
217
+
218
+ def reset_project(self) -> None:
219
+ """Remove the Reasoning Plus section from project settings.
220
+
221
+ If the settings file is empty after removal (only version remains), delete it.
222
+ """
223
+ if not self._project_dir:
224
+ return
225
+ path = self._project_dir / PROJECT_SETTINGS_FILENAME
226
+ settings = _read_json(path)
227
+ if settings and "reasoning_plus" in settings:
228
+ del settings["reasoning_plus"]
229
+ remaining_keys = [k for k in settings.keys() if k != "version"]
230
+ if remaining_keys:
231
+ _write_json(path, settings)
232
+ else:
233
+ try:
234
+ path.unlink()
235
+ except Exception:
236
+ pass
237
+
238
+
239
+ # ---------------------------------------------------------------------------
240
+ # Helpers
241
+ # ---------------------------------------------------------------------------
242
+
243
+ def _bool(value: Any) -> bool:
244
+ if isinstance(value, bool):
245
+ return value
246
+ if isinstance(value, str):
247
+ return value.strip().lower() in ("1", "true", "yes", "on")
248
+ return bool(value)
249
+
250
+
251
+ def _int(value: Any, default: int) -> int:
252
+ try:
253
+ return int(value)
254
+ except (TypeError, ValueError):
255
+ return default
256
+
257
+
258
+ def _float(value: Any, default: float) -> float:
259
+ try:
260
+ return float(value)
261
+ except (TypeError, ValueError):
262
+ return default
263
+
264
+
265
+ def _str(value: Any, default: str) -> str:
266
+ if isinstance(value, str):
267
+ return value.strip()
268
+ return default
269
+
270
+
271
+ def _optional_str(value: Any) -> str | None:
272
+ if value is None:
273
+ return None
274
+ if isinstance(value, str):
275
+ value = value.strip()
276
+ return value if value else None
277
+ return None
278
+
279
+
280
+ def _read_json(path: Path) -> dict[str, Any] | None:
281
+ if not path.exists():
282
+ return None
283
+ try:
284
+ with open(path, "r", encoding="utf-8") as f:
285
+ data = json.load(f)
286
+ if isinstance(data, dict):
287
+ return data
288
+ except Exception as exc:
289
+ logger.warning("thinkstack: failed to read settings from %s — %s", path, exc)
290
+ return None
291
+
292
+
293
+ def _write_json(path: Path, data: dict[str, Any]) -> None:
294
+ """Atomic JSON write (temp file + rename)."""
295
+ tmp = path.with_suffix(path.suffix + ".tmp")
296
+ try:
297
+ with open(tmp, "w", encoding="utf-8") as f:
298
+ json.dump(data, f, indent=2)
299
+ tmp.replace(path)
300
+ except Exception as exc:
301
+ logger.warning("thinkstack: failed to write settings to %s — %s", path, exc)
302
+ raise
303
+ finally:
304
+ if tmp.exists():
305
+ try:
306
+ tmp.unlink()
307
+ except Exception:
308
+ pass
309
+
310
+
311
+ def load_config(gcc_dir: Path | None = None) -> tuple[ReasoningPlusConfig, str]:
312
+ """Convenience function: load config + source."""
313
+ return SettingsStore(gcc_dir).load()