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,207 @@
1
+ """
2
+ REP ledger push/pull synchronization.
3
+
4
+ Protocol:
5
+ PUSH: POST /rep/entries body: {entries: [...REPEnvelope dicts...], sender_id: str}
6
+ PULL: GET /rep/entries?since=<index_or_0> → {entries: [...], node_id: str}
7
+
8
+ Sync is one-directional per call (push OR pull). Full sync = push then pull.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import urllib.error
15
+ import urllib.request
16
+ from dataclasses import dataclass, field
17
+ from datetime import datetime, timezone
18
+ from typing import List
19
+
20
+ from devtorch_core.rep import REPEnvelope, REPLedger
21
+ from devtorch_core.rep_network.node import NodeRegistry, PeerEntry
22
+
23
+
24
+ @dataclass
25
+ class SyncResult:
26
+ """Result of a push/pull sync operation with a single peer."""
27
+ peer_node_id: str
28
+ peer_url: str
29
+ pushed_count: int = 0
30
+ pulled_count: int = 0
31
+ errors: List[str] = field(default_factory=list)
32
+ success: bool = True
33
+
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Internal helpers
37
+ # ---------------------------------------------------------------------------
38
+
39
+ def _timestamp() -> str:
40
+ return datetime.now(timezone.utc).isoformat()
41
+
42
+
43
+ def _read_entries_from(ledger: REPLedger, since_index: int) -> List[dict]:
44
+ """Return ledger records (as dicts) with sequence_id > since_index."""
45
+ all_records = ledger.read_all()
46
+ return [r for r in all_records if r.get("sequence_id", 0) > since_index]
47
+
48
+
49
+ def _pulled_ids_path(ledger: REPLedger):
50
+ return ledger.rep_dir / "pulled_ids.json"
51
+
52
+
53
+ def _envelope_ids_in_ledger(ledger: REPLedger) -> set:
54
+ """Return set of envelope_ids already pulled (persisted sidecar + ledger records)."""
55
+ ids: set = set()
56
+ # Read from sidecar file (persists across calls)
57
+ p = _pulled_ids_path(ledger)
58
+ if p.exists():
59
+ try:
60
+ ids = set(json.loads(p.read_text(encoding="utf-8")))
61
+ except Exception:
62
+ pass
63
+ # Also check envelope.payload_hash in stored records for additional dedup
64
+ for record in ledger.read_all():
65
+ env = record.get("envelope", {})
66
+ eid = env.get("envelope_id")
67
+ if eid:
68
+ ids.add(eid)
69
+ return ids
70
+
71
+
72
+ def _persist_pulled_id(ledger: REPLedger, eid: str) -> None:
73
+ """Persist an envelope_id to the sidecar so future pulls skip it."""
74
+ p = _pulled_ids_path(ledger)
75
+ try:
76
+ existing: set = set(json.loads(p.read_text(encoding="utf-8"))) if p.exists() else set()
77
+ existing.add(eid)
78
+ p.write_text(json.dumps(list(existing)), encoding="utf-8")
79
+ except Exception:
80
+ pass
81
+
82
+
83
+ def _post_json(url: str, payload: dict, timeout: int = 10) -> dict:
84
+ data = json.dumps(payload).encode("utf-8")
85
+ req = urllib.request.Request(
86
+ url,
87
+ data=data,
88
+ headers={"Content-Type": "application/json"},
89
+ method="POST",
90
+ )
91
+ with urllib.request.urlopen(req, timeout=timeout) as resp: # noqa: S310
92
+ return json.loads(resp.read().decode("utf-8"))
93
+
94
+
95
+ def _get_json(url: str, timeout: int = 10) -> dict:
96
+ req = urllib.request.Request(url, method="GET")
97
+ with urllib.request.urlopen(req, timeout=timeout) as resp: # noqa: S310
98
+ return json.loads(resp.read().decode("utf-8"))
99
+
100
+
101
+ # ---------------------------------------------------------------------------
102
+ # Public API
103
+ # ---------------------------------------------------------------------------
104
+
105
+ def push_to_peer(
106
+ ledger: REPLedger,
107
+ peer: PeerEntry,
108
+ since_index: int = 0,
109
+ ) -> SyncResult:
110
+ """POST entries from ledger to peer. Never raises."""
111
+ result = SyncResult(peer_node_id=peer.node_id, peer_url=peer.url)
112
+ try:
113
+ records = _read_entries_from(ledger, since_index)
114
+ entries = [r.get("envelope", {}) for r in records]
115
+ if not entries:
116
+ result.pushed_count = 0
117
+ return result
118
+
119
+ identity_path = ledger.rep_dir / "node_id"
120
+ sender_id = identity_path.read_text().strip() if identity_path.exists() else ""
121
+
122
+ url = f"{peer.url.rstrip('/')}/rep/entries"
123
+ _post_json(url, {"entries": entries, "sender_id": sender_id})
124
+ result.pushed_count = len(entries)
125
+ except Exception as exc: # noqa: BLE001
126
+ result.success = False
127
+ result.errors.append(str(exc))
128
+ return result
129
+
130
+
131
+ def pull_from_peer(
132
+ ledger: REPLedger,
133
+ peer: PeerEntry,
134
+ since_index: int = 0,
135
+ ) -> SyncResult:
136
+ """GET entries from peer and append new ones to ledger. Never raises."""
137
+ result = SyncResult(peer_node_id=peer.node_id, peer_url=peer.url)
138
+ try:
139
+ url = f"{peer.url.rstrip('/')}/rep/entries?since={since_index}"
140
+ response = _get_json(url)
141
+ entries = response.get("entries", [])
142
+ if not entries:
143
+ return result
144
+
145
+ existing_ids = _envelope_ids_in_ledger(ledger)
146
+ pulled = 0
147
+ for entry_dict in entries:
148
+ eid = entry_dict.get("envelope_id")
149
+ if eid and eid in existing_ids:
150
+ continue
151
+ # Build envelope from the dict; tolerate extra keys
152
+ try:
153
+ envelope = REPEnvelope.from_dict(entry_dict)
154
+ except Exception: # noqa: BLE001
155
+ continue
156
+ ts = entry_dict.get("timestamp", _timestamp())
157
+ ledger.append(envelope, timestamp=ts)
158
+ if eid:
159
+ existing_ids.add(eid)
160
+ _persist_pulled_id(ledger, eid)
161
+ pulled += 1
162
+
163
+ result.pulled_count = pulled
164
+ except Exception as exc: # noqa: BLE001
165
+ result.success = False
166
+ result.errors.append(str(exc))
167
+ return result
168
+
169
+
170
+ def sync_with_peer(ledger: REPLedger, peer: PeerEntry) -> SyncResult:
171
+ """Push then pull with a single peer; merge into one SyncResult."""
172
+ push_result = push_to_peer(ledger, peer)
173
+ pull_result = pull_from_peer(ledger, peer)
174
+
175
+ merged = SyncResult(
176
+ peer_node_id=peer.node_id,
177
+ peer_url=peer.url,
178
+ pushed_count=push_result.pushed_count,
179
+ pulled_count=pull_result.pulled_count,
180
+ errors=push_result.errors + pull_result.errors,
181
+ success=push_result.success and pull_result.success,
182
+ )
183
+ return merged
184
+
185
+
186
+ def sync_all_peers(
187
+ ledger: REPLedger,
188
+ registry: NodeRegistry,
189
+ ) -> List[SyncResult]:
190
+ """Sync with every peer in registry. Never raises."""
191
+ results: List[SyncResult] = []
192
+ try:
193
+ peers = registry.list_peers()
194
+ except Exception: # noqa: BLE001
195
+ return results
196
+
197
+ for peer in peers:
198
+ try:
199
+ results.append(sync_with_peer(ledger, peer))
200
+ except Exception as exc: # noqa: BLE001
201
+ results.append(SyncResult(
202
+ peer_node_id=peer.node_id,
203
+ peer_url=peer.url,
204
+ success=False,
205
+ errors=[str(exc)],
206
+ ))
207
+ return results
@@ -0,0 +1,182 @@
1
+ """
2
+ Sprint 3 – Sensitivity schema and storage.
3
+
4
+ SensitivityEvent captures a single sensitivity observation emitted by a
5
+ reasoning node, including an optional numerical value, confidence, and a
6
+ disclosure level (PUBLIC / PROTECTED / PRIVATE).
7
+
8
+ SensitivityStore persists events as an append-only JSONL file under
9
+ `.GCC/sensitivities/events.jsonl` and maintains a concept-keyed index at
10
+ `.GCC/sensitivities/index.json` so callers can filter without full scans.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import dataclasses
16
+ import datetime as _dt
17
+ import json
18
+ import uuid
19
+ from pathlib import Path
20
+ from typing import List, Optional
21
+
22
+
23
+ DISCLOSURE_LEVELS = ("PUBLIC", "PROTECTED", "PRIVATE")
24
+
25
+
26
+ @dataclasses.dataclass
27
+ class SensitivityEvent:
28
+ """
29
+ A single sensitivity observation.
30
+
31
+ Fields
32
+ ------
33
+ source_node : Identifier of the reasoning node that emitted this event.
34
+ target_concept : The concept/variable whose sensitivity is being reported.
35
+ counterfactuals : Free-text description of the counterfactual question.
36
+ confidence : Confidence in the sensitivity estimate; float in [0, 1].
37
+ disclosure_level : One of PUBLIC, PROTECTED, PRIVATE.
38
+ numerical_value : Optional point-estimate of the sensitivity magnitude.
39
+ uncertainty : Optional ±uncertainty for numerical_value.
40
+ message : Human-readable annotation.
41
+ event_id : UUID assigned on creation.
42
+ created_at : ISO-8601 UTC timestamp.
43
+ """
44
+
45
+ source_node: str
46
+ target_concept: str
47
+ counterfactuals: str
48
+ confidence: float
49
+ disclosure_level: str
50
+ numerical_value: Optional[float] = None
51
+ uncertainty: Optional[float] = None
52
+ message: str = ""
53
+ event_id: str = dataclasses.field(
54
+ default_factory=lambda: str(uuid.uuid4())
55
+ )
56
+ created_at: str = dataclasses.field(
57
+ default_factory=lambda: _dt.datetime.now(tz=_dt.timezone.utc).isoformat()
58
+ )
59
+ agent_id: Optional[str] = None
60
+ session_id: Optional[str] = None
61
+
62
+ def __post_init__(self) -> None:
63
+ if self.disclosure_level not in DISCLOSURE_LEVELS:
64
+ raise ValueError(
65
+ f"disclosure_level must be one of {DISCLOSURE_LEVELS}, "
66
+ f"got '{self.disclosure_level}'"
67
+ )
68
+ if not (0.0 <= self.confidence <= 1.0):
69
+ raise ValueError(
70
+ f"confidence must be in [0, 1], got {self.confidence}"
71
+ )
72
+ if not self.source_node.strip():
73
+ raise ValueError("source_node must not be empty.")
74
+ if not self.target_concept.strip():
75
+ raise ValueError("target_concept must not be empty.")
76
+
77
+ def to_dict(self) -> dict:
78
+ return dataclasses.asdict(self)
79
+
80
+ @classmethod
81
+ def from_dict(cls, d: dict) -> "SensitivityEvent":
82
+ known = {f.name for f in dataclasses.fields(cls)}
83
+ return cls(**{k: v for k, v in d.items() if k in known})
84
+
85
+
86
+ class SensitivityStore:
87
+ """
88
+ Append-only storage for SensitivityEvent objects.
89
+
90
+ Layout (relative to the `.GCC/sensitivities/` directory):
91
+ events.jsonl – one JSON object per line, ordered by insertion.
92
+ index.json – concept_name → [event_id, …], rebuilt on every write.
93
+ """
94
+
95
+ EVENTS_FILE = "events.jsonl"
96
+ INDEX_FILE = "index.json"
97
+
98
+ def __init__(self, sensitivities_dir: Path) -> None:
99
+ self.dir = sensitivities_dir
100
+
101
+ def _ensure_dir(self) -> None:
102
+ self.dir.mkdir(parents=True, exist_ok=True)
103
+
104
+ @property
105
+ def events_path(self) -> Path:
106
+ return self.dir / self.EVENTS_FILE
107
+
108
+ @property
109
+ def index_path(self) -> Path:
110
+ return self.dir / self.INDEX_FILE
111
+
112
+ # ------------------------------------------------------------------
113
+ # Internal index helpers
114
+ # ------------------------------------------------------------------
115
+
116
+ def _load_index(self) -> dict:
117
+ if not self.index_path.exists():
118
+ return {}
119
+ try:
120
+ return json.loads(self.index_path.read_text(encoding="utf-8"))
121
+ except (json.JSONDecodeError, OSError):
122
+ return {}
123
+
124
+ def _save_index(self, index: dict) -> None:
125
+ self.index_path.write_text(
126
+ json.dumps(index, indent=2, sort_keys=True) + "\n",
127
+ encoding="utf-8",
128
+ )
129
+
130
+ # ------------------------------------------------------------------
131
+ # Public API
132
+ # ------------------------------------------------------------------
133
+
134
+ def add(self, event: SensitivityEvent) -> str:
135
+ """Persist *event* and update the concept index. Returns event_id."""
136
+ self._ensure_dir()
137
+ with self.events_path.open("a", encoding="utf-8") as fh:
138
+ fh.write(json.dumps(event.to_dict(), sort_keys=True) + "\n")
139
+
140
+ index = self._load_index()
141
+ index.setdefault(event.target_concept, []).append(event.event_id)
142
+ self._save_index(index)
143
+ return event.event_id
144
+
145
+ def list_all(self) -> List[SensitivityEvent]:
146
+ """Return all stored events in insertion order."""
147
+ if not self.events_path.exists():
148
+ return []
149
+ events: List[SensitivityEvent] = []
150
+ with self.events_path.open("r", encoding="utf-8") as fh:
151
+ for raw in fh:
152
+ line = raw.strip()
153
+ if not line:
154
+ continue
155
+ try:
156
+ events.append(SensitivityEvent.from_dict(json.loads(line)))
157
+ except (json.JSONDecodeError, TypeError, ValueError):
158
+ pass
159
+ return events
160
+
161
+ def filter(
162
+ self,
163
+ concept: Optional[str] = None,
164
+ disclosure_level: Optional[str] = None,
165
+ source_node: Optional[str] = None,
166
+ ) -> List[SensitivityEvent]:
167
+ """
168
+ Return events that match *all* provided filter criteria.
169
+ Any criterion that is None is treated as a wildcard.
170
+ """
171
+ events = self.list_all()
172
+ if concept is not None:
173
+ events = [e for e in events if e.target_concept == concept]
174
+ if disclosure_level is not None:
175
+ events = [e for e in events if e.disclosure_level == disclosure_level]
176
+ if source_node is not None:
177
+ events = [e for e in events if e.source_node == source_node]
178
+ return events
179
+
180
+ def concepts(self) -> List[str]:
181
+ """Return all distinct concept names seen so far."""
182
+ return sorted(self._load_index().keys())
devtorch_core/serve.py ADDED
@@ -0,0 +1,258 @@
1
+ """
2
+ Sprint 25 — Unified local dev server: `devtorch serve`.
3
+
4
+ Combines the existing FastAPI apps into a single local server:
5
+ - Proxy (mounted at /proxy)
6
+ - Dashboard (mounted at /dashboard)
7
+ - Broadcast (mounted at /broadcast)
8
+ - MCP (spawned as a stdio subprocess)
9
+
10
+ Usage:
11
+ from devtorch_core.serve import serve_all
12
+ serve_all(host="127.0.0.1", port=8765)
13
+
14
+ Or via the CLI helper (intended for integration in devtorch_cli/main.py):
15
+ from devtorch_core.serve import cmd_serve
16
+ cmd_serve(args)
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ import subprocess
22
+ import sys
23
+ from pathlib import Path
24
+ from typing import Any, Optional
25
+
26
+ from devtorch_core.broadcast.broadcaster import create_broadcast_app, EventBroadcaster
27
+ from devtorch_core.dashboard_api import create_app as create_dashboard_app
28
+ from devtorch_core.proxy.server import create_app as create_proxy_app
29
+ from devtorch_core.proxy.server import find_gcc_repo as proxy_find_gcc_repo
30
+
31
+ try:
32
+ from fastapi import FastAPI
33
+ import uvicorn
34
+
35
+ _HAS_FASTAPI = True
36
+ except ImportError: # pragma: no cover
37
+ _HAS_FASTAPI = False
38
+
39
+
40
+ DEFAULT_HOST = "127.0.0.1"
41
+ DEFAULT_PORT = 8765
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # GCC repository resolution
46
+ # ---------------------------------------------------------------------------
47
+
48
+
49
+ def _resolve_gcc_repo(gcc_path: Optional[str] = None) -> Optional[Any]:
50
+ """
51
+ Resolve an initialised GCCRepository.
52
+
53
+ Search order:
54
+ 1. Explicit `gcc_path` argument.
55
+ 2. DEVTORCH_GCC_PATH environment variable.
56
+ 3. Walk upward from cwd (proxy server discovery).
57
+ """
58
+ from devtorch_core import GCCRepository
59
+
60
+ candidates: list[str] = []
61
+ if gcc_path is not None:
62
+ candidates.append(str(gcc_path))
63
+ explicit = os.environ.get("DEVTORCH_GCC_PATH", "").strip()
64
+ if explicit:
65
+ candidates.append(explicit)
66
+
67
+ for candidate in candidates:
68
+ try:
69
+ repo = GCCRepository.at(Path(candidate))
70
+ if repo.is_initialized():
71
+ return repo
72
+ except Exception:
73
+ continue
74
+
75
+ return proxy_find_gcc_repo()
76
+
77
+
78
+ def _repo_path(repo: Optional[Any]) -> Optional[Path]:
79
+ """Best-effort extraction of a Path from a GCCRepository-like object."""
80
+ if repo is None:
81
+ return None
82
+ if hasattr(repo, "root"):
83
+ return Path(repo.root)
84
+ if hasattr(repo, "path"):
85
+ return Path(repo.path)
86
+ return Path(repo)
87
+
88
+
89
+ # ---------------------------------------------------------------------------
90
+ # App factory
91
+ # ---------------------------------------------------------------------------
92
+
93
+
94
+ def build_app(
95
+ gcc_repo: Optional[Any] = None,
96
+ broadcaster: Optional[EventBroadcaster] = None,
97
+ mcp_process: Optional[subprocess.Popen] = None,
98
+ ) -> "FastAPI":
99
+ """
100
+ Build a single FastAPI app combining proxy, dashboard, and broadcast.
101
+
102
+ Parameters
103
+ ----------
104
+ gcc_repo:
105
+ An initialised GCCRepository used by the proxy and dashboard.
106
+ broadcaster:
107
+ Optional EventBroadcaster instance; a fresh one is created if None.
108
+ mcp_process:
109
+ Optional subprocess.Popen handle for the stdio MCP server. If provided,
110
+ the unified /health endpoint reports its runtime status.
111
+ """
112
+ if not _HAS_FASTAPI:
113
+ raise ImportError(
114
+ "FastAPI and uvicorn are required for the unified server. "
115
+ "Install them with: pip install fastapi uvicorn"
116
+ )
117
+
118
+ app = FastAPI(
119
+ title="DevTorch Unified Local Server",
120
+ version="0.1.0",
121
+ description=(
122
+ "Local dev server that unifies the DevTorch proxy, dashboard API, "
123
+ "SSE broadcaster, and MCP server into a single process."
124
+ ),
125
+ )
126
+
127
+ # Proxy app (mounted at /proxy)
128
+ proxy_app = create_proxy_app(gcc_repo)
129
+ app.mount("/proxy", proxy_app)
130
+
131
+ # Dashboard app (mounted at /dashboard)
132
+ dashboard_path = _repo_path(gcc_repo)
133
+ dashboard_app = create_dashboard_app(dashboard_path)
134
+ app.mount("/dashboard", dashboard_app)
135
+
136
+ # Broadcast app (mounted at /broadcast)
137
+ broadcast_broadcaster = broadcaster or EventBroadcaster()
138
+ broadcast_app = create_broadcast_app(broadcast_broadcaster)
139
+ if broadcast_app is not None:
140
+ app.mount("/broadcast", broadcast_app)
141
+
142
+ # Keep the MCP subprocess handle accessible to the health endpoint.
143
+ if mcp_process is not None:
144
+ app.state.mcp_process = mcp_process
145
+
146
+ @app.get("/health")
147
+ def health() -> dict:
148
+ """Aggregate health check for all mounted sub-services."""
149
+ mcp_status = "not_started"
150
+ mcp_proc = getattr(app.state, "mcp_process", None)
151
+ if mcp_proc is not None:
152
+ try:
153
+ mcp_status = "running" if mcp_proc.poll() is None else "stopped"
154
+ except Exception:
155
+ mcp_status = "unknown"
156
+
157
+ return {
158
+ "status": "ok",
159
+ "proxy": "ok",
160
+ "dashboard": "ok",
161
+ "broadcast": "ok",
162
+ "mcp": mcp_status,
163
+ }
164
+
165
+ return app
166
+
167
+
168
+ # ---------------------------------------------------------------------------
169
+ # Server runner
170
+ # ---------------------------------------------------------------------------
171
+
172
+
173
+ def serve_all(
174
+ host: str = DEFAULT_HOST,
175
+ port: int = DEFAULT_PORT,
176
+ gcc_path: Optional[str] = None,
177
+ ) -> None:
178
+ """
179
+ Start the unified FastAPI app and spawn the stdio MCP server as a subprocess.
180
+
181
+ The MCP server is launched with `python -m devtorch_core.mcp.server` using
182
+ the same interpreter and inherits the current environment. If a GCC repo
183
+ is resolved, DEVTORCH_GCC_PATH is set so the MCP server can find it.
184
+
185
+ Parameters
186
+ ----------
187
+ host:
188
+ Host interface to bind (default 127.0.0.1).
189
+ port:
190
+ Port to bind (default 8765).
191
+ gcc_path:
192
+ Optional project root containing an initialised `.GCC/` directory.
193
+ """
194
+ if not _HAS_FASTAPI:
195
+ raise ImportError(
196
+ "FastAPI and uvicorn are required for the unified server. "
197
+ "Install them with: pip install fastapi uvicorn"
198
+ )
199
+
200
+ repo = _resolve_gcc_repo(gcc_path)
201
+
202
+ env = dict(os.environ)
203
+ if repo is not None:
204
+ repo_path = _repo_path(repo)
205
+ if repo_path is not None:
206
+ env["DEVTORCH_GCC_PATH"] = str(repo_path)
207
+ elif gcc_path is not None:
208
+ env["DEVTORCH_GCC_PATH"] = str(gcc_path)
209
+
210
+ mcp_cmd = [sys.executable, "-m", "devtorch_core.mcp.server"]
211
+ mcp_process = subprocess.Popen(
212
+ mcp_cmd,
213
+ stdin=subprocess.PIPE,
214
+ stdout=subprocess.PIPE,
215
+ stderr=subprocess.PIPE,
216
+ env=env,
217
+ )
218
+
219
+ app = build_app(gcc_repo=repo, mcp_process=mcp_process)
220
+
221
+ print(f"DevTorch unified server listening on http://{host}:{port}")
222
+ print(f" Proxy: http://{host}:{port}/proxy")
223
+ print(f" Dashboard: http://{host}:{port}/dashboard")
224
+ print(f" Broadcast: http://{host}:{port}/broadcast")
225
+ print(f" MCP (stdio): spawned pid={mcp_process.pid}")
226
+
227
+ try:
228
+ uvicorn.run(app, host=host, port=port, log_level="warning")
229
+ finally:
230
+ if mcp_process.poll() is None:
231
+ mcp_process.terminate()
232
+ try:
233
+ mcp_process.wait(timeout=5)
234
+ except subprocess.TimeoutExpired:
235
+ mcp_process.kill()
236
+ mcp_process.wait()
237
+
238
+
239
+ # ---------------------------------------------------------------------------
240
+ # CLI helper (for later integration in devtorch_cli/main.py)
241
+ # ---------------------------------------------------------------------------
242
+
243
+
244
+ def cmd_serve(args: Any) -> int:
245
+ """
246
+ Adapter to call `serve_all` from the CLI argument parser.
247
+
248
+ Expected attributes on `args`:
249
+ - host (str)
250
+ - port (int)
251
+ - gcc_path (Optional[str])
252
+ Missing attributes fall back to defaults.
253
+ """
254
+ host = getattr(args, "host", DEFAULT_HOST)
255
+ port = int(getattr(args, "port", DEFAULT_PORT))
256
+ gcc_path = getattr(args, "gcc_path", None)
257
+ serve_all(host=host, port=port, gcc_path=gcc_path)
258
+ return 0
@@ -0,0 +1,39 @@
1
+ """Multi-agent session support: planning, disagreement resolution, and what-if simulation."""
2
+
3
+ from .models import (
4
+ ACTIVE,
5
+ COMPLETED,
6
+ FAILED,
7
+ PENDING,
8
+ PAUSED,
9
+ PLANNING,
10
+ AgentAssignment,
11
+ Session,
12
+ SessionStatus,
13
+ Subtask,
14
+ is_terminal_status,
15
+ )
16
+ from .orchestrator import SessionOrchestrator
17
+ from .planner import SubtaskPlanner
18
+ from .disagreement import DisagreementRecord, DisagreementResolver
19
+ from .simulator import SimulationResult, WhatIfSimulator
20
+
21
+ __all__ = [
22
+ "SessionStatus",
23
+ "PENDING",
24
+ "PLANNING",
25
+ "ACTIVE",
26
+ "PAUSED",
27
+ "COMPLETED",
28
+ "FAILED",
29
+ "AgentAssignment",
30
+ "Subtask",
31
+ "Session",
32
+ "is_terminal_status",
33
+ "SessionOrchestrator",
34
+ "SubtaskPlanner",
35
+ "DisagreementRecord",
36
+ "DisagreementResolver",
37
+ "SimulationResult",
38
+ "WhatIfSimulator",
39
+ ]