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.
- thinkstack_core/__init__.py +158 -0
- thinkstack_core/aggphi_textual.py +275 -0
- thinkstack_core/alerts/__init__.py +23 -0
- thinkstack_core/alerts/base.py +46 -0
- thinkstack_core/alerts/config.py +60 -0
- thinkstack_core/alerts/dispatcher.py +110 -0
- thinkstack_core/alerts/jira.py +96 -0
- thinkstack_core/alerts/linear.py +72 -0
- thinkstack_core/alerts/pagerduty.py +66 -0
- thinkstack_core/alerts/slack.py +81 -0
- thinkstack_core/alerts/teams.py +70 -0
- thinkstack_core/audit/__init__.py +43 -0
- thinkstack_core/audit/exporter.py +297 -0
- thinkstack_core/audit/privacy.py +101 -0
- thinkstack_core/audit/scrubber.py +149 -0
- thinkstack_core/audit/service.py +67 -0
- thinkstack_core/audit/signing.py +127 -0
- thinkstack_core/broadcast/__init__.py +4 -0
- thinkstack_core/broadcast/broadcaster.py +100 -0
- thinkstack_core/broadcast/watcher.py +71 -0
- thinkstack_core/capability.py +639 -0
- thinkstack_core/cloud/__init__.py +1 -0
- thinkstack_core/cloud/client_config.py +472 -0
- thinkstack_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
- thinkstack_core/cloud/client_configs/.claude-stdio.json +13 -0
- thinkstack_core/cloud/client_configs/.cursor-mcp.json +13 -0
- thinkstack_core/cloud/client_configs/.opencode-bridge.json +13 -0
- thinkstack_core/cloud/client_configs/.opencode.json +15 -0
- thinkstack_core/cloud/client_configs/.vscode-mcp.json +13 -0
- thinkstack_core/cloud/mcp_client.py +229 -0
- thinkstack_core/cloud/setup.py +144 -0
- thinkstack_core/cloud/sync.py +143 -0
- thinkstack_core/cloud/sync_bundle.py +639 -0
- thinkstack_core/cloud/sync_conflicts.py +183 -0
- thinkstack_core/cloud/sync_state.py +159 -0
- thinkstack_core/cloud/team_sync.py +337 -0
- thinkstack_core/cloud/thinkstack-mcp-bridge.js +357 -0
- thinkstack_core/codex/__init__.py +9 -0
- thinkstack_core/codex/__main__.py +97 -0
- thinkstack_core/codex/capture.py +208 -0
- thinkstack_core/codex/proxy.py +412 -0
- thinkstack_core/compat.py +103 -0
- thinkstack_core/concept_catalog.py +209 -0
- thinkstack_core/consolidation/__init__.py +3 -0
- thinkstack_core/consolidation/synthesizer.py +87 -0
- thinkstack_core/consolidation/workflow.py +175 -0
- thinkstack_core/daemon/__init__.py +27 -0
- thinkstack_core/daemon/supervisor.py +293 -0
- thinkstack_core/daemon/watcher.py +244 -0
- thinkstack_core/dashboard_api.py +2012 -0
- thinkstack_core/deltaf.py +97 -0
- thinkstack_core/disclosure.py +50 -0
- thinkstack_core/divergence/__init__.py +3 -0
- thinkstack_core/divergence/detector.py +166 -0
- thinkstack_core/gateway/__init__.py +32 -0
- thinkstack_core/gateway/key_manager.py +124 -0
- thinkstack_core/gateway/metrics_webhook.py +252 -0
- thinkstack_core/gateway/policy.py +262 -0
- thinkstack_core/gateway/server.py +727 -0
- thinkstack_core/gateway/sso.py +233 -0
- thinkstack_core/gcc.py +1246 -0
- thinkstack_core/github/__init__.py +35 -0
- thinkstack_core/github/app.py +240 -0
- thinkstack_core/github/comment_builder.py +113 -0
- thinkstack_core/github/pat.py +76 -0
- thinkstack_core/github/pr_parser.py +82 -0
- thinkstack_core/github/pr_reporter.py +555 -0
- thinkstack_core/gitlab/__init__.py +177 -0
- thinkstack_core/hitl/__init__.py +4 -0
- thinkstack_core/hitl/channels.py +129 -0
- thinkstack_core/hitl/orchestrator.py +95 -0
- thinkstack_core/hooks/__init__.py +17 -0
- thinkstack_core/hooks/claude_code.py +228 -0
- thinkstack_core/hooks/git_capture.py +341 -0
- thinkstack_core/hooks/git_commit.py +182 -0
- thinkstack_core/hooks/installer.py +850 -0
- thinkstack_core/hooks/pre_commit.py +157 -0
- thinkstack_core/hooks/runner.py +386 -0
- thinkstack_core/identity/__init__.py +4 -0
- thinkstack_core/identity/agent.py +86 -0
- thinkstack_core/identity/providers.py +85 -0
- thinkstack_core/invariants.py +182 -0
- thinkstack_core/mcp/__init__.py +10 -0
- thinkstack_core/mcp/auth.py +177 -0
- thinkstack_core/mcp/server.py +1215 -0
- thinkstack_core/metrics/__init__.py +35 -0
- thinkstack_core/metrics/aggregate.py +215 -0
- thinkstack_core/metrics/calibrate.py +198 -0
- thinkstack_core/metrics/calibration.py +125 -0
- thinkstack_core/metrics/credibility.py +288 -0
- thinkstack_core/metrics/delivery_time.py +70 -0
- thinkstack_core/metrics/dhs.py +126 -0
- thinkstack_core/metrics/mcs.py +96 -0
- thinkstack_core/metrics/roi.py +88 -0
- thinkstack_core/metrics/session_writer.py +81 -0
- thinkstack_core/metrics/shadow_ai.py +117 -0
- thinkstack_core/metrics/sprint_writer.py +243 -0
- thinkstack_core/observability/__init__.py +78 -0
- thinkstack_core/observability/datadog.py +157 -0
- thinkstack_core/observability/formatter.py +119 -0
- thinkstack_core/observability/report.py +264 -0
- thinkstack_core/observability/servicenow.py +147 -0
- thinkstack_core/observability/splunk.py +218 -0
- thinkstack_core/observability/webhook.py +227 -0
- thinkstack_core/parser/__init__.py +30 -0
- thinkstack_core/parser/blocks.py +216 -0
- thinkstack_core/parser/inference.py +159 -0
- thinkstack_core/parser/thinking.py +112 -0
- thinkstack_core/projects.py +169 -0
- thinkstack_core/prompt_artifact.py +76 -0
- thinkstack_core/proxy/__init__.py +9 -0
- thinkstack_core/proxy/routes/__init__.py +1 -0
- thinkstack_core/proxy/routes/anthropic.py +264 -0
- thinkstack_core/proxy/routes/azure_openai.py +336 -0
- thinkstack_core/proxy/routes/gemini.py +331 -0
- thinkstack_core/proxy/routes/groq.py +284 -0
- thinkstack_core/proxy/routes/ollama.py +279 -0
- thinkstack_core/proxy/routes/openai.py +287 -0
- thinkstack_core/proxy/server.py +356 -0
- thinkstack_core/query/__init__.py +15 -0
- thinkstack_core/query/grep.py +181 -0
- thinkstack_core/query/hybrid.py +86 -0
- thinkstack_core/query/semantic.py +157 -0
- thinkstack_core/rdp.py +105 -0
- thinkstack_core/reasoning/__init__.py +4 -0
- thinkstack_core/reasoning/entry.py +31 -0
- thinkstack_core/reasoning/store.py +122 -0
- thinkstack_core/reasoning_plus/__init__.py +70 -0
- thinkstack_core/reasoning_plus/augmenter.py +337 -0
- thinkstack_core/reasoning_plus/capture.py +51 -0
- thinkstack_core/reasoning_plus/config.py +313 -0
- thinkstack_core/reasoning_plus/context.py +262 -0
- thinkstack_core/reasoning_plus/learning/__init__.py +125 -0
- thinkstack_core/reasoning_plus/learning/analytics.py +141 -0
- thinkstack_core/reasoning_plus/learning/api.py +784 -0
- thinkstack_core/reasoning_plus/learning/chain.py +285 -0
- thinkstack_core/reasoning_plus/learning/composer.py +141 -0
- thinkstack_core/reasoning_plus/learning/conflicts.py +184 -0
- thinkstack_core/reasoning_plus/learning/context_collector.py +194 -0
- thinkstack_core/reasoning_plus/learning/cross_project.py +234 -0
- thinkstack_core/reasoning_plus/learning/deny_list.py +108 -0
- thinkstack_core/reasoning_plus/learning/embeddings.py +209 -0
- thinkstack_core/reasoning_plus/learning/evolution.py +119 -0
- thinkstack_core/reasoning_plus/learning/extractor.py +271 -0
- thinkstack_core/reasoning_plus/learning/filter_five_layer.py +95 -0
- thinkstack_core/reasoning_plus/learning/models.py +149 -0
- thinkstack_core/reasoning_plus/learning/org_store.py +156 -0
- thinkstack_core/reasoning_plus/learning/pii.py +142 -0
- thinkstack_core/reasoning_plus/learning/promotion.py +58 -0
- thinkstack_core/reasoning_plus/learning/provenance.py +126 -0
- thinkstack_core/reasoning_plus/learning/recorder.py +81 -0
- thinkstack_core/reasoning_plus/learning/relevance.py +122 -0
- thinkstack_core/reasoning_plus/learning/state.py +86 -0
- thinkstack_core/reasoning_plus/learning/store.py +178 -0
- thinkstack_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
- thinkstack_core/reasoning_plus/prompt.py +90 -0
- thinkstack_core/rep.py +134 -0
- thinkstack_core/rep_network/__init__.py +25 -0
- thinkstack_core/rep_network/merge.py +70 -0
- thinkstack_core/rep_network/node.py +137 -0
- thinkstack_core/rep_network/server.py +140 -0
- thinkstack_core/rep_network/sync.py +207 -0
- thinkstack_core/sensitivity.py +182 -0
- thinkstack_core/serve.py +258 -0
- thinkstack_core/session/__init__.py +39 -0
- thinkstack_core/session/disagreement.py +188 -0
- thinkstack_core/session/models.py +114 -0
- thinkstack_core/session/orchestrator.py +182 -0
- thinkstack_core/session/planner.py +169 -0
- thinkstack_core/session/simulator.py +132 -0
- thinkstack_core/signing.py +290 -0
- thinkstack_core/sis.py +197 -0
- thinkstack_core/skills/pr-reviewer/SKILL.md +204 -0
- thinkstack_core/skills/thinkstack-auto-sync/SKILL.md +175 -0
- thinkstack_core/skills/thinkstack-session-start/SKILL.md +136 -0
- thinkstack_core/storage.py +308 -0
- thinkstack_core/templates/__init__.py +6 -0
- thinkstack_core/templates/engine.py +122 -0
- thinkstack_core/templates/go.py +18 -0
- thinkstack_core/templates/infra.py +19 -0
- thinkstack_core/templates/library/__init__.py +18 -0
- thinkstack_core/templates/library/api_design.md +27 -0
- thinkstack_core/templates/library/bug_fix.md +27 -0
- thinkstack_core/templates/library/decision_record.md +27 -0
- thinkstack_core/templates/library/engine.py +228 -0
- thinkstack_core/templates/library/security_review.md +30 -0
- thinkstack_core/templates/python.py +19 -0
- thinkstack_core/templates/react.py +18 -0
- thinkstack_core/templates/typescript.py +18 -0
- thinkstack_core/theta.py +221 -0
- thinkstack_core/theta_synthesis.py +268 -0
- thinkstack_core/topics.py +320 -0
- thinkstack_core/variance.py +219 -0
- thinkstack_core/wrapper/__init__.py +52 -0
- thinkstack_core/wrapper/anthropic.py +487 -0
- thinkstack_core/wrapper/base.py +562 -0
- thinkstack_core/wrapper/bedrock.py +342 -0
- thinkstack_core/wrapper/gemini.py +422 -0
- thinkstack_core/wrapper/ollama.py +527 -0
- thinkstack_core/wrapper/openai.py +461 -0
- thinkstack_core-4.0.0.dist-info/METADATA +868 -0
- thinkstack_core-4.0.0.dist-info/RECORD +205 -0
- thinkstack_core-4.0.0.dist-info/WHEEL +5 -0
- thinkstack_core-4.0.0.dist-info/entry_points.txt +2 -0
- thinkstack_core-4.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""
|
|
2
|
+
thinkstack_core.concept_catalog
|
|
3
|
+
==============================
|
|
4
|
+
Unified concept enumeration across all ThinkStack data sources.
|
|
5
|
+
|
|
6
|
+
Sources:
|
|
7
|
+
- I3 concept definitions (.GCC/concepts/*.json)
|
|
8
|
+
- Theta coordination vector (.GCC/theta.json)
|
|
9
|
+
- Sensitivity events (.GCC/sensitivities/events.jsonl)
|
|
10
|
+
- Learning trigger_concepts (.GCC/reasoning_learnings/learnings/*.json)
|
|
11
|
+
- Commit concepts (.GCC/commits/*.json)
|
|
12
|
+
- Call concepts (.GCC/reasoning_learnings/calls/*.json)
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from thinkstack_core.topics import TopicStore
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger("thinkstack.concept_catalog")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ConceptCatalog:
|
|
27
|
+
"""Unify all concept sources into a single enumerator."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, gcc_dir: Path | str) -> None:
|
|
30
|
+
self._gcc_dir = Path(gcc_dir)
|
|
31
|
+
|
|
32
|
+
def enumerate_all(self) -> dict[str, list[str]]:
|
|
33
|
+
"""Return {concept_name: [source1, source2, ...]} from all sources.
|
|
34
|
+
|
|
35
|
+
All concept names are lowercased for deduplication.
|
|
36
|
+
"""
|
|
37
|
+
result: dict[str, list[str]] = {}
|
|
38
|
+
|
|
39
|
+
def _add(concept: str, source: str) -> None:
|
|
40
|
+
key = concept.lower()
|
|
41
|
+
if key not in result:
|
|
42
|
+
result[key] = []
|
|
43
|
+
if source not in result[key]:
|
|
44
|
+
result[key].append(source)
|
|
45
|
+
|
|
46
|
+
for name in self._i3_concepts():
|
|
47
|
+
_add(name, "i3_definition")
|
|
48
|
+
for name in self._theta_concepts():
|
|
49
|
+
_add(name, "theta")
|
|
50
|
+
for name in self._sensitivity_concepts():
|
|
51
|
+
_add(name, "sensitivity")
|
|
52
|
+
for name in self._learning_concepts():
|
|
53
|
+
_add(name, "learning")
|
|
54
|
+
for name in self._commit_concepts():
|
|
55
|
+
_add(name, "commit")
|
|
56
|
+
for name in self._call_concepts():
|
|
57
|
+
_add(name, "call")
|
|
58
|
+
|
|
59
|
+
return dict(sorted(result.items()))
|
|
60
|
+
|
|
61
|
+
def enumerate_learned(self) -> dict[str, dict[str, Any]]:
|
|
62
|
+
"""Return {concept: {learning_count, active, stale, deprecated, success_rate, avg_confidence}}.
|
|
63
|
+
|
|
64
|
+
Only concepts that have at least one learning are included.
|
|
65
|
+
"""
|
|
66
|
+
from thinkstack_core.reasoning_plus.learning.analytics import concept_stats
|
|
67
|
+
from thinkstack_core.reasoning_plus.learning.store import LearningStore
|
|
68
|
+
|
|
69
|
+
store = LearningStore(self._gcc_dir)
|
|
70
|
+
stats = concept_stats(store)
|
|
71
|
+
result: dict[str, dict[str, Any]] = {}
|
|
72
|
+
for cname, data in stats.items():
|
|
73
|
+
if cname == "__untyped__":
|
|
74
|
+
continue
|
|
75
|
+
result[cname] = {
|
|
76
|
+
"learning_count": data["total"],
|
|
77
|
+
"active": data["active"],
|
|
78
|
+
"stale": data["stale"],
|
|
79
|
+
"deprecated": data["deprecated"],
|
|
80
|
+
"success_rate": data["success_rate"],
|
|
81
|
+
"avg_confidence": data["avg_confidence"],
|
|
82
|
+
}
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
def concepts_for_topic(
|
|
86
|
+
self, topic: str, hide_empty: bool = True,
|
|
87
|
+
) -> dict[str, Any]:
|
|
88
|
+
"""Return concepts associated with a topic.
|
|
89
|
+
|
|
90
|
+
Returns a dict with:
|
|
91
|
+
- "concepts": list of {name, sources, has_data} for concepts with data
|
|
92
|
+
- "taxonomy_only": list of concept names that have no data (if hide_empty)
|
|
93
|
+
"""
|
|
94
|
+
store = TopicStore(self._gcc_dir)
|
|
95
|
+
concepts = store.get_concepts(topic)
|
|
96
|
+
if not concepts:
|
|
97
|
+
return {"concepts": [], "taxonomy_only": []}
|
|
98
|
+
|
|
99
|
+
all_concepts = self.enumerate_all()
|
|
100
|
+
learned = self.enumerate_learned()
|
|
101
|
+
|
|
102
|
+
with_data: list[dict[str, Any]] = []
|
|
103
|
+
without_data: list[str] = []
|
|
104
|
+
|
|
105
|
+
for concept in concepts:
|
|
106
|
+
sources = all_concepts.get(concept.lower(), [])
|
|
107
|
+
if sources:
|
|
108
|
+
entry: dict[str, Any] = {"name": concept, "sources": sources}
|
|
109
|
+
if concept.lower() in learned:
|
|
110
|
+
stats = learned[concept.lower()]
|
|
111
|
+
entry["learning_count"] = stats["learning_count"]
|
|
112
|
+
entry["success_rate"] = stats["success_rate"]
|
|
113
|
+
entry["avg_confidence"] = stats["avg_confidence"]
|
|
114
|
+
with_data.append(entry)
|
|
115
|
+
else:
|
|
116
|
+
without_data.append(concept)
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
"concepts": with_data,
|
|
120
|
+
"taxonomy_only": without_data if hide_empty else [],
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
def topic_for_concept(self, concept: str) -> str | None:
|
|
124
|
+
"""Reverse lookup: which topic does this concept belong to?"""
|
|
125
|
+
store = TopicStore(self._gcc_dir)
|
|
126
|
+
cname = concept.lower()
|
|
127
|
+
|
|
128
|
+
for topic in store.list_topics():
|
|
129
|
+
topic_concepts = [c.lower() for c in topic["concepts"]]
|
|
130
|
+
if cname in topic_concepts:
|
|
131
|
+
return topic["name"]
|
|
132
|
+
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
def _i3_concepts(self) -> list[str]:
|
|
136
|
+
concepts_dir = self._gcc_dir / "concepts"
|
|
137
|
+
if not concepts_dir.exists():
|
|
138
|
+
return []
|
|
139
|
+
return [p.stem for p in concepts_dir.glob("*.json")]
|
|
140
|
+
|
|
141
|
+
def _theta_concepts(self) -> list[str]:
|
|
142
|
+
path = self._gcc_dir / "theta.json"
|
|
143
|
+
if not path.exists():
|
|
144
|
+
return []
|
|
145
|
+
try:
|
|
146
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
147
|
+
return list(data.get("coordination_vector", {}).keys())
|
|
148
|
+
except (json.JSONDecodeError, OSError):
|
|
149
|
+
return []
|
|
150
|
+
|
|
151
|
+
def _sensitivity_concepts(self) -> list[str]:
|
|
152
|
+
path = self._gcc_dir / "sensitivities" / "events.jsonl"
|
|
153
|
+
if not path.exists():
|
|
154
|
+
return []
|
|
155
|
+
concepts: set[str] = set()
|
|
156
|
+
try:
|
|
157
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
158
|
+
if not line.strip():
|
|
159
|
+
continue
|
|
160
|
+
ev = json.loads(line)
|
|
161
|
+
payload = ev.get("payload", ev)
|
|
162
|
+
concept = payload.get("target_concept", "")
|
|
163
|
+
if concept:
|
|
164
|
+
concepts.add(concept)
|
|
165
|
+
except (json.JSONDecodeError, OSError):
|
|
166
|
+
pass
|
|
167
|
+
return list(concepts)
|
|
168
|
+
|
|
169
|
+
def _learning_concepts(self) -> list[str]:
|
|
170
|
+
path = self._gcc_dir / "reasoning_learnings" / "learnings"
|
|
171
|
+
if not path.exists():
|
|
172
|
+
return []
|
|
173
|
+
concepts: set[str] = set()
|
|
174
|
+
for p in path.glob("*.json"):
|
|
175
|
+
try:
|
|
176
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
177
|
+
for c in data.get("trigger_concepts", []):
|
|
178
|
+
concepts.add(c)
|
|
179
|
+
except (json.JSONDecodeError, OSError):
|
|
180
|
+
continue
|
|
181
|
+
return list(concepts)
|
|
182
|
+
|
|
183
|
+
def _commit_concepts(self) -> list[str]:
|
|
184
|
+
path = self._gcc_dir / "commits"
|
|
185
|
+
if not path.exists():
|
|
186
|
+
return []
|
|
187
|
+
concepts: set[str] = set()
|
|
188
|
+
for p in path.glob("*.json"):
|
|
189
|
+
try:
|
|
190
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
191
|
+
for c in data.get("concepts", []):
|
|
192
|
+
concepts.add(c)
|
|
193
|
+
except (json.JSONDecodeError, OSError):
|
|
194
|
+
continue
|
|
195
|
+
return list(concepts)
|
|
196
|
+
|
|
197
|
+
def _call_concepts(self) -> list[str]:
|
|
198
|
+
path = self._gcc_dir / "reasoning_learnings" / "calls"
|
|
199
|
+
if not path.exists():
|
|
200
|
+
return []
|
|
201
|
+
concepts: set[str] = set()
|
|
202
|
+
for p in path.glob("*.json"):
|
|
203
|
+
try:
|
|
204
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
205
|
+
for c in data.get("concepts", []):
|
|
206
|
+
concepts.add(c)
|
|
207
|
+
except (json.JSONDecodeError, OSError):
|
|
208
|
+
continue
|
|
209
|
+
return list(concepts)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLM-powered consolidation synthesizer.
|
|
3
|
+
|
|
4
|
+
Uses the same pattern as theta_synthesis.py: builds a prompt from
|
|
5
|
+
ReasoningEntries + DivergenceSignals, sends to claude-haiku, returns
|
|
6
|
+
a ConsolidationDraft. Gated by THINKSTACK_DISABLE env var.
|
|
7
|
+
Never raises — errors return a skip result.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import dataclasses
|
|
12
|
+
import os
|
|
13
|
+
from typing import List
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclasses.dataclass
|
|
17
|
+
class ConsolidationDraft:
|
|
18
|
+
decision: str
|
|
19
|
+
decision_trace: str
|
|
20
|
+
model: str
|
|
21
|
+
tokens_used: int
|
|
22
|
+
skipped: bool = False
|
|
23
|
+
skip_reason: str = ""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def synthesise_consolidation(
|
|
27
|
+
concept: str,
|
|
28
|
+
reasoning_entries: list,
|
|
29
|
+
divergence_signals: list,
|
|
30
|
+
model: str = "claude-haiku-4-5-20251001",
|
|
31
|
+
) -> ConsolidationDraft:
|
|
32
|
+
"""Run LLM synthesis over reasoning entries + divergence signals."""
|
|
33
|
+
if os.environ.get("THINKSTACK_DISABLE"):
|
|
34
|
+
return ConsolidationDraft(decision="", decision_trace="", model=model, tokens_used=0,
|
|
35
|
+
skipped=True, skip_reason="THINKSTACK_DISABLE set")
|
|
36
|
+
try:
|
|
37
|
+
import anthropic
|
|
38
|
+
except ImportError:
|
|
39
|
+
return ConsolidationDraft(decision="", decision_trace="", model=model, tokens_used=0,
|
|
40
|
+
skipped=True, skip_reason="anthropic SDK not installed")
|
|
41
|
+
|
|
42
|
+
prompt = _build_prompt(concept, reasoning_entries, divergence_signals)
|
|
43
|
+
try:
|
|
44
|
+
client = anthropic.Anthropic()
|
|
45
|
+
response = client.messages.create(
|
|
46
|
+
model=model,
|
|
47
|
+
max_tokens=512,
|
|
48
|
+
messages=[{"role": "user", "content": prompt}],
|
|
49
|
+
)
|
|
50
|
+
text = response.content[0].text if response.content else ""
|
|
51
|
+
tokens = response.usage.input_tokens + response.usage.output_tokens
|
|
52
|
+
decision, trace = _parse_response(text)
|
|
53
|
+
return ConsolidationDraft(decision=decision, decision_trace=trace,
|
|
54
|
+
model=model, tokens_used=tokens)
|
|
55
|
+
except Exception as exc:
|
|
56
|
+
return ConsolidationDraft(decision="", decision_trace="", model=model, tokens_used=0,
|
|
57
|
+
skipped=True, skip_reason=str(exc))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _build_prompt(concept: str, reasoning_entries: list, divergence_signals: list) -> str:
|
|
61
|
+
entry_lines = "\n".join(
|
|
62
|
+
f" [{i+1}] agent={e.agent_id} conf={e.confidence:.2f}: {e.reasoning_text[:200]}"
|
|
63
|
+
for i, e in enumerate(reasoning_entries[:10])
|
|
64
|
+
)
|
|
65
|
+
signal_lines = "\n".join(
|
|
66
|
+
f" [{s.signal_type}/{s.severity}] {s.description}"
|
|
67
|
+
for s in divergence_signals[:5]
|
|
68
|
+
)
|
|
69
|
+
return (
|
|
70
|
+
f"You are a governance analyst. Agents have diverged on concept: '{concept}'.\n\n"
|
|
71
|
+
f"Agent reasoning:\n{entry_lines or ' (none)'}\n\n"
|
|
72
|
+
f"Divergence signals:\n{signal_lines or ' (none)'}\n\n"
|
|
73
|
+
"Write two sections:\n"
|
|
74
|
+
"DECISION: A 1-2 sentence consolidated position that resolves the divergence.\n"
|
|
75
|
+
"TRACE: A 1-2 sentence explanation of WHY this consolidation was reached.\n\n"
|
|
76
|
+
"Format exactly:\nDECISION: <text>\nTRACE: <text>"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _parse_response(text: str) -> tuple[str, str]:
|
|
81
|
+
decision, trace = "", ""
|
|
82
|
+
for line in text.splitlines():
|
|
83
|
+
if line.startswith("DECISION:"):
|
|
84
|
+
decision = line[len("DECISION:"):].strip()
|
|
85
|
+
elif line.startswith("TRACE:"):
|
|
86
|
+
trace = line[len("TRACE:"):].strip()
|
|
87
|
+
return decision or text[:200], trace or "LLM synthesis"
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import datetime as _dt
|
|
5
|
+
import json
|
|
6
|
+
import uuid
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
from thinkstack_core.divergence.detector import DivergenceSignal
|
|
11
|
+
|
|
12
|
+
_CONSOLIDATIONS_DIR = "consolidations"
|
|
13
|
+
|
|
14
|
+
_SEVERITY_ORDER = {"HIGH": 0, "MEDIUM": 1, "LOW": 2}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclasses.dataclass
|
|
18
|
+
class ConsolidationRecord:
|
|
19
|
+
"""
|
|
20
|
+
Durable record of a consolidation event.
|
|
21
|
+
|
|
22
|
+
decision_trace is ThinkStack's own reasoning — WHY it chose to block,
|
|
23
|
+
escalate to human, or auto-resolve.
|
|
24
|
+
"""
|
|
25
|
+
record_id: str
|
|
26
|
+
concept: str
|
|
27
|
+
signals: List[dict] # serialised DivergenceSignal dicts
|
|
28
|
+
state: str # drafting | pending_human | auto_resolved | resolved | blocked
|
|
29
|
+
decision: str # consolidated position text
|
|
30
|
+
decision_trace: str # ThinkStack's reasoning for its action
|
|
31
|
+
agents_involved: List[str]
|
|
32
|
+
human_comments: List[dict]
|
|
33
|
+
created_at: str
|
|
34
|
+
resolved_at: Optional[str] = None
|
|
35
|
+
|
|
36
|
+
def to_dict(self) -> dict:
|
|
37
|
+
return dataclasses.asdict(self)
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def from_dict(cls, d: dict) -> "ConsolidationRecord":
|
|
41
|
+
known = {f.name for f in dataclasses.fields(cls)}
|
|
42
|
+
return cls(**{k: v for k, v in d.items() if k in known})
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ConsolidationWorkflow:
|
|
46
|
+
"""
|
|
47
|
+
State machine for agent reasoning consolidation.
|
|
48
|
+
|
|
49
|
+
Transitions:
|
|
50
|
+
drafting → pending_human (HIGH or MEDIUM severity divergence)
|
|
51
|
+
drafting → auto_resolved (LOW severity only)
|
|
52
|
+
drafting → blocked (signal type E: consensus contradiction)
|
|
53
|
+
pending_human → resolved (human provides decision via add_human_comment)
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, repo) -> None:
|
|
57
|
+
self._repo = repo
|
|
58
|
+
self._gcc_dir = repo.gcc_dir
|
|
59
|
+
self._dir = self._gcc_dir / _CONSOLIDATIONS_DIR
|
|
60
|
+
self._dir.mkdir(exist_ok=True)
|
|
61
|
+
|
|
62
|
+
def start(
|
|
63
|
+
self,
|
|
64
|
+
concept: str,
|
|
65
|
+
signals: List[DivergenceSignal],
|
|
66
|
+
) -> ConsolidationRecord:
|
|
67
|
+
"""Create a ConsolidationRecord and determine initial state from signals."""
|
|
68
|
+
record_id = str(uuid.uuid4())
|
|
69
|
+
now = _dt.datetime.now(tz=_dt.timezone.utc).isoformat()
|
|
70
|
+
|
|
71
|
+
all_agents = list({aid for s in signals for aid in s.agent_ids})
|
|
72
|
+
max_severity = self._max_severity(signals)
|
|
73
|
+
|
|
74
|
+
# E-type signals always block
|
|
75
|
+
e_signals = [s for s in signals if s.signal_type == "E"]
|
|
76
|
+
if e_signals:
|
|
77
|
+
state = "blocked"
|
|
78
|
+
decision = ""
|
|
79
|
+
trace = (
|
|
80
|
+
f"Blocked: {len(e_signals)} consensus contradiction(s) detected on '{concept}'. "
|
|
81
|
+
"Human review required before proceeding."
|
|
82
|
+
)
|
|
83
|
+
elif max_severity == "LOW":
|
|
84
|
+
state = "auto_resolved"
|
|
85
|
+
decision = self._auto_decision(concept, signals)
|
|
86
|
+
trace = (
|
|
87
|
+
f"Auto-resolved: all signals LOW severity on '{concept}'. "
|
|
88
|
+
f"Signals: {', '.join(s.signal_type for s in signals)}. "
|
|
89
|
+
"No human intervention required."
|
|
90
|
+
)
|
|
91
|
+
else:
|
|
92
|
+
state = "pending_human"
|
|
93
|
+
decision = ""
|
|
94
|
+
trace = (
|
|
95
|
+
f"Escalated to human: {max_severity} severity signal(s) detected on '{concept}'. "
|
|
96
|
+
f"Signal types: {', '.join(s.signal_type + '(' + s.severity + ')' for s in signals)}. "
|
|
97
|
+
"Awaiting human comment or resolution."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
record = ConsolidationRecord(
|
|
101
|
+
record_id=record_id,
|
|
102
|
+
concept=concept,
|
|
103
|
+
signals=[dataclasses.asdict(s) for s in signals],
|
|
104
|
+
state=state,
|
|
105
|
+
decision=decision,
|
|
106
|
+
decision_trace=trace,
|
|
107
|
+
agents_involved=all_agents,
|
|
108
|
+
human_comments=[],
|
|
109
|
+
created_at=now,
|
|
110
|
+
resolved_at=now if state == "auto_resolved" else None,
|
|
111
|
+
)
|
|
112
|
+
self._save(record)
|
|
113
|
+
return record
|
|
114
|
+
|
|
115
|
+
def add_human_comment(
|
|
116
|
+
self,
|
|
117
|
+
record_id: str,
|
|
118
|
+
author: str,
|
|
119
|
+
comment: str,
|
|
120
|
+
decision: Optional[str] = None,
|
|
121
|
+
) -> ConsolidationRecord:
|
|
122
|
+
"""Add a human comment and optionally resolve the record."""
|
|
123
|
+
record = self.get(record_id)
|
|
124
|
+
if record is None:
|
|
125
|
+
raise ValueError(f"ConsolidationRecord {record_id!r} not found")
|
|
126
|
+
|
|
127
|
+
now = _dt.datetime.now(tz=_dt.timezone.utc).isoformat()
|
|
128
|
+
record.human_comments.append({"author": author, "comment": comment, "timestamp": now})
|
|
129
|
+
|
|
130
|
+
if decision is not None:
|
|
131
|
+
record.decision = decision
|
|
132
|
+
record.state = "resolved"
|
|
133
|
+
record.resolved_at = now
|
|
134
|
+
record.decision_trace += f"\nResolved by {author} at {now[:19]}: {decision}"
|
|
135
|
+
|
|
136
|
+
self._save(record)
|
|
137
|
+
return record
|
|
138
|
+
|
|
139
|
+
def get(self, record_id: str) -> Optional[ConsolidationRecord]:
|
|
140
|
+
path = self._dir / f"{record_id}.json"
|
|
141
|
+
if not path.exists():
|
|
142
|
+
return None
|
|
143
|
+
try:
|
|
144
|
+
return ConsolidationRecord.from_dict(json.loads(path.read_text(encoding="utf-8")))
|
|
145
|
+
except (json.JSONDecodeError, TypeError):
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
def list_for_concept(self, concept: str) -> List[ConsolidationRecord]:
|
|
149
|
+
results = []
|
|
150
|
+
for f in self._dir.glob("*.json"):
|
|
151
|
+
try:
|
|
152
|
+
d = json.loads(f.read_text(encoding="utf-8"))
|
|
153
|
+
if d.get("concept") == concept:
|
|
154
|
+
results.append(ConsolidationRecord.from_dict(d))
|
|
155
|
+
except (json.JSONDecodeError, TypeError):
|
|
156
|
+
continue
|
|
157
|
+
return sorted(results, key=lambda r: r.created_at, reverse=True)
|
|
158
|
+
|
|
159
|
+
def _save(self, record: ConsolidationRecord) -> None:
|
|
160
|
+
path = self._dir / f"{record.record_id}.json"
|
|
161
|
+
path.write_text(json.dumps(record.to_dict(), indent=2) + "\n", encoding="utf-8")
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def _max_severity(signals: List[DivergenceSignal]) -> str:
|
|
165
|
+
if not signals:
|
|
166
|
+
return "LOW"
|
|
167
|
+
return min(signals, key=lambda s: _SEVERITY_ORDER.get(s.severity, 99)).severity
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
def _auto_decision(concept: str, signals: List[DivergenceSignal]) -> str:
|
|
171
|
+
return (
|
|
172
|
+
f"No high-severity divergence detected on '{concept}'. "
|
|
173
|
+
"Agents may proceed with existing sensitivity signals. "
|
|
174
|
+
f"Low-severity signals noted: {', '.join(s.description[:80] for s in signals)}."
|
|
175
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""ThinkStack background daemon (S26).
|
|
2
|
+
|
|
3
|
+
Provides a long-running filesystem watcher that emits `fs_change` events to
|
|
4
|
+
`.GCC/events.log.jsonl` without requiring IDE hooks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from thinkstack_core.daemon.supervisor import (
|
|
8
|
+
PID_FILE_NAME,
|
|
9
|
+
LOG_FILE_NAME,
|
|
10
|
+
start_daemon,
|
|
11
|
+
stop_daemon,
|
|
12
|
+
daemon_status,
|
|
13
|
+
daemon_logs,
|
|
14
|
+
_run_daemon,
|
|
15
|
+
)
|
|
16
|
+
from thinkstack_core.daemon.watcher import ProjectWatcher
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"PID_FILE_NAME",
|
|
20
|
+
"LOG_FILE_NAME",
|
|
21
|
+
"start_daemon",
|
|
22
|
+
"stop_daemon",
|
|
23
|
+
"daemon_status",
|
|
24
|
+
"daemon_logs",
|
|
25
|
+
"_run_daemon",
|
|
26
|
+
"ProjectWatcher",
|
|
27
|
+
]
|