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,159 @@
1
+ """
2
+ devtorch_core.cloud.sync_conflicts
3
+ ====================================
4
+ Append-only, hash-chained conflict log for sync conflicts.
5
+
6
+ Each conflict entry records what was found on both sides, what resolution
7
+ was applied, and whether it has been manually reviewed.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import json
13
+ import logging
14
+ from collections.abc import Sequence
15
+ from datetime import datetime, timezone
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ logger = logging.getLogger("devtorch.cloud.sync_conflicts")
20
+
21
+ _CONFLICTS_FILE = "sync_conflicts.jsonl"
22
+
23
+
24
+ class ConflictLog:
25
+ """Append, list, and resolve sync conflicts."""
26
+
27
+ def __init__(self, gcc_dir: Path | str) -> None:
28
+ self._gcc_dir = Path(gcc_dir)
29
+ self._path = self._gcc_dir / _CONFLICTS_FILE
30
+
31
+ def append(self, conflict: dict) -> str:
32
+ """Append a conflict entry (hash-chained). Returns conflict_id."""
33
+ entry = dict(conflict)
34
+ if "conflict_id" not in entry:
35
+ from uuid import uuid4
36
+ entry["conflict_id"] = f"conf_{uuid4().hex[:12]}"
37
+ entry["timestamp"] = entry.get("timestamp", _now_iso())
38
+
39
+ last_hash = self._last_hash()
40
+ prior = last_hash or "null"
41
+ entry["prior_hash"] = prior
42
+ entry["hash"] = _compute_hash(json.dumps(entry, separators=(",", ":")))
43
+
44
+ self._gcc_dir.mkdir(parents=True, exist_ok=True)
45
+ with open(str(self._path), "a", encoding="utf-8") as f:
46
+ f.write(json.dumps(entry, separators=(",", ":")) + "\n")
47
+
48
+ return entry["conflict_id"]
49
+
50
+ def list_unresolved(self) -> list[dict]:
51
+ """Return all unresolved conflicts."""
52
+ return [c for c in self._read_all() if not c.get("resolved")]
53
+
54
+ def list_all(self) -> list[dict]:
55
+ """Return all conflicts (resolved + unresolved)."""
56
+ return self._read_all()
57
+
58
+ def resolve(self, conflict_id: str, use_remote: bool = False, dismiss: bool = False) -> dict | None:
59
+ """Mark a conflict as resolved. If *use_remote*, returns the remote_value for application.
60
+
61
+ Returns the conflict dict (with resolution fields) if found, or None if not found.
62
+ """
63
+ all_entries = self._read_all()
64
+ found = None
65
+ for entry in all_entries:
66
+ if entry.get("conflict_id") == conflict_id:
67
+ entry["resolved"] = True
68
+ entry["resolved_at"] = _now_iso()
69
+ if use_remote:
70
+ entry["resolution"] = "used_remote"
71
+ elif dismiss:
72
+ entry["resolution"] = "dismissed"
73
+ else:
74
+ entry["resolution"] = entry.get("resolution", "kept_local")
75
+ found = entry
76
+ break
77
+
78
+ if found is None:
79
+ return None
80
+
81
+ self._rewrite_all(all_entries)
82
+ if use_remote:
83
+ return found
84
+ return found
85
+
86
+ def count_unresolved(self) -> int:
87
+ """Return count of unresolved conflicts."""
88
+ return len(self.list_unresolved())
89
+
90
+ def summary(self) -> str:
91
+ """Return a formatted summary for the ``devtorch today`` dashboard."""
92
+ unresolved = self.list_unresolved()
93
+ if not unresolved:
94
+ return ""
95
+ high = sum(1 for c in unresolved if c.get("severity") == "high")
96
+ medium = sum(1 for c in unresolved if c.get("severity") == "medium")
97
+ parts = [f" \u26a0 {len(unresolved)} unresolved conflict(s)"]
98
+ for c in unresolved[:5]:
99
+ parts.append(f" [{c.get('severity','?')}] {c.get('type','?')} — {c.get('item_id','?')}")
100
+ if len(unresolved) > 5:
101
+ parts.append(f" ... and {len(unresolved) - 5} more")
102
+ parts.append(" Run 'devtorch sync conflicts' for details.")
103
+ return "\n".join(["─── Sync conflicts ───"] + parts)
104
+
105
+ def _read_all(self) -> list[dict]:
106
+ if not self._path.exists():
107
+ return []
108
+ entries: list[dict] = []
109
+ try:
110
+ for line in self._path.read_text(encoding="utf-8").splitlines():
111
+ if not line.strip():
112
+ continue
113
+ try:
114
+ entries.append(json.loads(line))
115
+ except json.JSONDecodeError:
116
+ continue
117
+ except OSError:
118
+ pass
119
+ return entries
120
+
121
+ def _rewrite_all(self, entries: Sequence[dict]) -> None:
122
+ content = "\n".join(json.dumps(e, separators=(",", ":")) for e in entries) + "\n"
123
+ _atomic_write(self._path, content)
124
+
125
+ def _last_hash(self) -> str | None:
126
+ if not self._path.exists():
127
+ return None
128
+ try:
129
+ last_line = ""
130
+ with open(str(self._path), "r", encoding="utf-8") as f:
131
+ for line in f:
132
+ if line.strip():
133
+ last_line = line.strip()
134
+ if last_line:
135
+ return json.loads(last_line).get("hash")
136
+ except (OSError, json.JSONDecodeError):
137
+ pass
138
+ return None
139
+
140
+
141
+ def _now_iso() -> str:
142
+ return datetime.now(timezone.utc).isoformat()
143
+
144
+
145
+ def _compute_hash(data: str) -> str:
146
+ return hashlib.sha256(data.encode("utf-8")).hexdigest()[:16]
147
+
148
+
149
+ def _atomic_write(path: Path, content: str) -> None:
150
+ tmp = path.with_suffix(path.suffix + ".tmp")
151
+ try:
152
+ tmp.write_text(content, encoding="utf-8")
153
+ tmp.replace(path)
154
+ finally:
155
+ if tmp.exists():
156
+ try:
157
+ tmp.unlink()
158
+ except Exception:
159
+ pass
@@ -0,0 +1,159 @@
1
+ """
2
+ devtorch_core.cloud.sync_state
3
+ ===============================
4
+ Read, update, and persist sync state at .GCC/sync_state.json.
5
+
6
+ Tracks what has been synced between local .GCC/ and the MCP server for
7
+ each data type (commits, theta, learnings, sensitivities, topics).
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import logging
13
+ from datetime import datetime, timezone
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ logger = logging.getLogger("devtorch.cloud.sync_state")
18
+
19
+
20
+ class SyncState:
21
+ """Read, update, and persist sync state at .GCC/sync_state.json."""
22
+
23
+ def __init__(self, gcc_dir: Path | str) -> None:
24
+ self._gcc_dir = Path(gcc_dir)
25
+ self._path = self._gcc_dir / "sync_state.json"
26
+
27
+ def load(self) -> dict:
28
+ """Load sync state, returning empty state if file doesn't exist."""
29
+ if not self._path.exists():
30
+ return self._empty_state()
31
+ try:
32
+ data = json.loads(self._path.read_text(encoding="utf-8"))
33
+ if not isinstance(data, dict):
34
+ return self._empty_state()
35
+ return data
36
+ except (json.JSONDecodeError, OSError):
37
+ return self._empty_state()
38
+
39
+ def save(self, state: dict) -> None:
40
+ """Persist sync state atomically."""
41
+ self._gcc_dir.mkdir(parents=True, exist_ok=True)
42
+ state["last_sync_at"] = _now_iso()
43
+ _atomic_write(self._path, json.dumps(state, indent=2) + "\n")
44
+
45
+ def update_after_pull(self, state: dict, pull_result: dict) -> dict:
46
+ """Update cursors after a successful pull."""
47
+ server_state = pull_result.get("server_state", {})
48
+
49
+ commits_state = state.setdefault("commits", {})
50
+ server_commits = server_state.get("commits", {})
51
+ commits_state["remote_head"] = server_commits.get("head", "0")
52
+ synced_ids = set(commits_state.get("synced_ids", []))
53
+ for c in pull_result.get("delta", {}).get("commits", []):
54
+ cid = c.get("id")
55
+ if cid:
56
+ synced_ids.add(cid)
57
+ commits_state["synced_ids"] = sorted(synced_ids)
58
+
59
+ state["theta"] = server_state.get("theta", {})
60
+
61
+ learnings_state = state.setdefault("learnings", {})
62
+ server_learnings = server_state.get("learnings", {})
63
+ synced_lids = set(learnings_state.get("synced_ids", []))
64
+ for l in pull_result.get("delta", {}).get("learnings", []):
65
+ lid = l.get("id")
66
+ if lid:
67
+ synced_lids.add(lid)
68
+ learnings_state["synced_ids"] = sorted(synced_lids)
69
+ learnings_state["synced_count"] = len(synced_lids)
70
+
71
+ sensitivities_state = state.setdefault("sensitivities", {})
72
+ sensitivities_state["last_synced_ts"] = server_state.get("sensitivities", {}).get("last_synced_ts", "")
73
+
74
+ topics_state = state.setdefault("topics", {})
75
+ synced_topics = set(topics_state.get("synced_custom_names", []))
76
+ for name in server_state.get("topics", {}).get("synced_custom_names", []):
77
+ synced_topics.add(name)
78
+ topics_state["synced_custom_names"] = sorted(synced_topics)
79
+
80
+ return state
81
+
82
+ def update_after_push(self, state: dict, push_result: dict, server_state: dict) -> dict:
83
+ """Update cursors after a successful push."""
84
+ commits_state = state.setdefault("commits", {})
85
+ synced_ids = set(commits_state.get("synced_ids", []))
86
+ for c in push_result.get("delta", {}).get("commits", []):
87
+ cid = c.get("id")
88
+ if cid:
89
+ synced_ids.add(cid)
90
+ commits_state["synced_ids"] = sorted(synced_ids)
91
+ commits_state["remote_head"] = server_state.get("commits", {}).get("head", "0")
92
+
93
+ learnings_state = state.setdefault("learnings", {})
94
+ synced_lids = set(learnings_state.get("synced_ids", []))
95
+ for l in push_result.get("delta", {}).get("learnings", []):
96
+ lid = l.get("id")
97
+ if lid:
98
+ synced_lids.add(lid)
99
+ learnings_state["synced_ids"] = sorted(synced_lids)
100
+ learnings_state["synced_count"] = len(synced_lids)
101
+
102
+ state["theta"] = server_state.get("theta", state.get("theta", {}))
103
+
104
+ sensitivities_state = state.setdefault("sensitivities", {})
105
+ sensitivities_state["last_synced_ts"] = server_state.get("sensitivities", {}).get("last_synced_ts", "")
106
+
107
+ topics_state = state.setdefault("topics", {})
108
+ synced_topics = set(topics_state.get("synced_custom_names", []))
109
+ for name in server_state.get("topics", {}).get("synced_custom_names", []):
110
+ synced_topics.add(name)
111
+ topics_state["synced_custom_names"] = sorted(synced_topics)
112
+
113
+ return state
114
+
115
+ def get_remote_state_view(self, state: dict) -> dict:
116
+ """Return the subset of state representing what the remote has."""
117
+ commits_state = state.get("commits", {})
118
+ return {
119
+ "commits": {
120
+ "synced_ids": commits_state.get("synced_ids", []),
121
+ "remote_head": commits_state.get("remote_head", "0"),
122
+ },
123
+ "theta": state.get("theta", {}),
124
+ "learnings": {"synced_ids": state.get("learnings", {}).get("synced_ids", [])},
125
+ "sensitivities": {"last_synced_ts": state.get("sensitivities", {}).get("last_synced_ts", "")},
126
+ "topics": {"synced_custom_names": state.get("topics", {}).get("synced_custom_names", [])},
127
+ }
128
+
129
+ def reset(self, state: dict) -> dict:
130
+ """Reset sync state to empty (triggers full re-sync on next sync)."""
131
+ return self._empty_state()
132
+
133
+ def _empty_state(self) -> dict:
134
+ return {
135
+ "version": "1",
136
+ "last_sync_at": "",
137
+ "commits": {"synced_ids": [], "remote_head": "0"},
138
+ "theta": {},
139
+ "learnings": {"synced_ids": [], "synced_count": 0},
140
+ "sensitivities": {"last_synced_ts": ""},
141
+ "topics": {"synced_custom_names": []},
142
+ }
143
+
144
+
145
+ def _now_iso() -> str:
146
+ return datetime.now(timezone.utc).isoformat()
147
+
148
+
149
+ def _atomic_write(path: Path, content: str) -> None:
150
+ tmp = path.with_suffix(path.suffix + ".tmp")
151
+ try:
152
+ tmp.write_text(content, encoding="utf-8")
153
+ tmp.replace(path)
154
+ finally:
155
+ if tmp.exists():
156
+ try:
157
+ tmp.unlink()
158
+ except Exception:
159
+ pass
@@ -0,0 +1,283 @@
1
+ """Team-level Θ (coordination vector) synchronization.
2
+
3
+ Provides ``push_theta`` and ``pull_theta`` primitives that store the org-level
4
+ Θ in the same cloud storage backend used by the DevTorch MCP server. Each repo
5
+ pushes its local ``.GCC/theta.json`` to ``team_theta/<org_id>.json`` and pulls
6
+ the remote org Θ back, merging it with the local copy using the same
7
+ ``merge_theta`` logic used by the REP network.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import os
14
+ from pathlib import Path
15
+ from typing import TYPE_CHECKING
16
+
17
+ from devtorch_core.rep_network.merge import merge_theta
18
+ from devtorch_core.storage import create_backend_from_env
19
+ from devtorch_core.theta import THETA_FILE
20
+
21
+ if TYPE_CHECKING:
22
+ from devtorch_core.storage import StorageBackend
23
+
24
+ TEAM_THETA_PREFIX = "team_theta"
25
+
26
+
27
+ def _theta_remote_key(org_id: str) -> str:
28
+ return f"{TEAM_THETA_PREFIX}/{org_id}.json"
29
+
30
+
31
+ def _load_local_theta(gcc_dir: Path) -> dict:
32
+ path = gcc_dir / THETA_FILE
33
+ if not path.exists():
34
+ return {"version": 1, "updated_at": None, "coordination_vector": {}}
35
+ try:
36
+ return json.loads(path.read_text(encoding="utf-8"))
37
+ except (json.JSONDecodeError, OSError):
38
+ return {"version": 1, "updated_at": None, "coordination_vector": {}}
39
+
40
+
41
+ def _save_local_theta(gcc_dir: Path, theta: dict) -> None:
42
+ path = gcc_dir / THETA_FILE
43
+ path.parent.mkdir(parents=True, exist_ok=True)
44
+ path.write_text(json.dumps(theta, indent=2, sort_keys=True) + "\n", encoding="utf-8")
45
+
46
+
47
+ def _get_backend(org_id: str) -> "StorageBackend":
48
+ return create_backend_from_env(prefix=org_id)
49
+
50
+
51
+ def push_theta(org_id: str, gcc_dir: str | Path) -> dict:
52
+ """Push the local ``.GCC/theta.json`` to the org's team Θ store.
53
+
54
+ Uses read-merge-write to avoid destructive overwrite of concurrent pushes.
55
+ Local theta is merged into the remote store using the same ``merge_theta``
56
+ logic used by ``pull_theta``, so concurrent pushes from different repos
57
+ do not lose data.
58
+
59
+ Args:
60
+ org_id: DevTorch org ID.
61
+ gcc_dir: Path to the local ``.GCC/`` directory.
62
+
63
+ Returns:
64
+ A status dict with the number of concepts pushed.
65
+ """
66
+ backend = _get_backend(org_id)
67
+ local_path = Path(gcc_dir)
68
+ local_theta = _load_local_theta(local_path)
69
+ remote_key = _theta_remote_key(org_id)
70
+
71
+ try:
72
+ remote_bytes = backend.read_bytes(remote_key)
73
+ remote_theta = json.loads(remote_bytes.decode("utf-8"))
74
+ except (FileNotFoundError, json.JSONDecodeError, OSError):
75
+ remote_theta = {"version": 1, "updated_at": None, "coordination_vector": {}}
76
+
77
+ merged = merge_theta(local_theta, remote_theta)
78
+ backend.write_bytes(
79
+ remote_key,
80
+ json.dumps(merged, indent=2, sort_keys=True).encode("utf-8"),
81
+ )
82
+ concepts = len(local_theta.get("coordination_vector", {}))
83
+ return {
84
+ "org_id": org_id,
85
+ "remote_key": remote_key,
86
+ "concepts_pushed": concepts,
87
+ }
88
+
89
+
90
+ def pull_theta(org_id: str, gcc_dir: str | Path) -> dict:
91
+ """Pull the org's team Θ and merge it into the local ``.GCC/theta.json``.
92
+
93
+ Args:
94
+ org_id: DevTorch org ID.
95
+ gcc_dir: Path to the local ``.GCC/`` directory.
96
+
97
+ Returns:
98
+ A status dict with the number of concepts before and after the merge.
99
+ """
100
+ backend = _get_backend(org_id)
101
+ local_path = Path(gcc_dir)
102
+ local_theta = _load_local_theta(local_path)
103
+ remote_key = _theta_remote_key(org_id)
104
+
105
+ try:
106
+ remote_bytes = backend.read_bytes(remote_key)
107
+ except FileNotFoundError:
108
+ remote_theta = {"version": 1, "updated_at": None, "coordination_vector": {}}
109
+ else:
110
+ remote_theta = json.loads(remote_bytes.decode("utf-8"))
111
+
112
+ before = len(local_theta.get("coordination_vector", {}))
113
+ merged = merge_theta(local_theta, remote_theta)
114
+ _save_local_theta(local_path, merged)
115
+ after = len(merged.get("coordination_vector", {}))
116
+ return {
117
+ "org_id": org_id,
118
+ "remote_key": remote_key,
119
+ "concepts_before": before,
120
+ "concepts_after": after,
121
+ "concepts_added": after - before,
122
+ }
123
+
124
+
125
+ # ------------------------------------------------------------------
126
+ # Cross-project learning sync
127
+ # ------------------------------------------------------------------
128
+
129
+ TEAM_LEARNINGS_PREFIX = "team_learnings"
130
+
131
+
132
+ def _learnings_remote_key(org_id: str) -> str:
133
+ return f"{TEAM_LEARNINGS_PREFIX}/{org_id}.json"
134
+
135
+
136
+ def _load_local_learnings(gcc_dir: Path) -> list[dict]:
137
+ """Load and serialize all learnings from a local .GCC/ into dicts."""
138
+ from devtorch_core.reasoning_plus.learning.store import LearningStore
139
+ store = LearningStore(gcc_dir)
140
+ return [l.to_dict() for l in store.list()]
141
+
142
+
143
+ def _save_remote_learnings(
144
+ backend: "StorageBackend",
145
+ remote_key: str,
146
+ learnings: list[dict],
147
+ ) -> None:
148
+ """Atomically write learnings to the remote key.
149
+
150
+ Each learning dict carries a ``_source_gcc_dir`` field to track origin.
151
+ """
152
+ data = {
153
+ "version": "1",
154
+ "updated_at": _now_iso(),
155
+ "learnings": learnings,
156
+ }
157
+ backend.write_bytes(
158
+ remote_key,
159
+ json.dumps(data, indent=2).encode("utf-8"),
160
+ )
161
+
162
+
163
+ def _load_remote_learnings(
164
+ backend: "StorageBackend",
165
+ remote_key: str,
166
+ ) -> list[dict]:
167
+ """Load learnings from the remote key, returning a list of dicts."""
168
+ try:
169
+ raw_bytes = backend.read_bytes(remote_key)
170
+ except FileNotFoundError:
171
+ return []
172
+ try:
173
+ data = json.loads(raw_bytes.decode("utf-8"))
174
+ return data.get("learnings", [])
175
+ except (json.JSONDecodeError, OSError, KeyError):
176
+ return []
177
+
178
+
179
+ def _merge_learnings(
180
+ local: list[dict],
181
+ remote: list[dict],
182
+ ) -> list[dict]:
183
+ """Merge remote learnings into local, keyed by learning ID.
184
+
185
+ Remote learnings with IDs not present locally are added.
186
+ Remote learnings with IDs already present are skipped (local wins).
187
+ """
188
+ local_ids = {l.get("id") for l in local if l.get("id")}
189
+ merged = list(local)
190
+ for rl in remote:
191
+ rid = rl.get("id")
192
+ if rid and rid not in local_ids:
193
+ merged.append(rl)
194
+ local_ids.add(rid)
195
+ return merged
196
+
197
+
198
+ def push_learnings(org_id: str, gcc_dir: str | Path) -> dict:
199
+ """Push local learnings to the org's team learning store.
200
+
201
+ Args:
202
+ org_id: DevTorch org ID.
203
+ gcc_dir: Path to the local .GCC/ directory.
204
+
205
+ Returns:
206
+ A status dict with the number of learnings pushed.
207
+ """
208
+ backend = _get_backend(org_id)
209
+ local_path = Path(gcc_dir)
210
+ local_learnings = _load_local_learnings(local_path)
211
+
212
+ for l in local_learnings:
213
+ l["_source_gcc_dir"] = str(local_path)
214
+
215
+ remote_learnings = _load_remote_learnings(backend, _learnings_remote_key(org_id))
216
+ merged = _merge_learnings(remote_learnings, local_learnings)
217
+ _save_remote_learnings(backend, _learnings_remote_key(org_id), merged)
218
+
219
+ return {
220
+ "org_id": org_id,
221
+ "remote_key": _learnings_remote_key(org_id),
222
+ "learnings_pushed": len(local_learnings),
223
+ "learnings_total_remote": len(merged),
224
+ }
225
+
226
+
227
+ def pull_learnings(org_id: str, gcc_dir: str | Path) -> dict:
228
+ """Pull team learnings from the org store and add new ones to the local store.
229
+
230
+ Remote learnings that do not exist locally (by ID) are saved.
231
+ Remote learnings with IDs already present are skipped (local wins).
232
+
233
+ Args:
234
+ org_id: DevTorch org ID.
235
+ gcc_dir: Path to the local .GCC/ directory.
236
+
237
+ Returns:
238
+ A status dict with the number of learnings added locally.
239
+ """
240
+ from devtorch_core.reasoning_plus.learning.models import Learning
241
+ from devtorch_core.reasoning_plus.learning.store import LearningStore
242
+
243
+ backend = _get_backend(org_id)
244
+ local_path = Path(gcc_dir)
245
+ store = LearningStore(local_path)
246
+ local_learnings = store.list()
247
+ local_ids = {l.id for l in local_learnings}
248
+
249
+ remote_learnings = _load_remote_learnings(backend, _learnings_remote_key(org_id))
250
+ added = 0
251
+ for rl in remote_learnings:
252
+ rid = rl.get("id")
253
+ if rid and rid not in local_ids:
254
+ try:
255
+ learning = Learning.from_dict(rl)
256
+ store.save(learning)
257
+ local_ids.add(rid)
258
+ added += 1
259
+ except Exception as exc:
260
+ logger.warning("devtorch: failed to save remote learning %s — %s", rid, exc)
261
+
262
+ return {
263
+ "org_id": org_id,
264
+ "remote_key": _learnings_remote_key(org_id),
265
+ "learnings_added": added,
266
+ "learnings_total_local": len(local_learnings) + added,
267
+ }
268
+
269
+
270
+ def _now_iso() -> str:
271
+ from datetime import datetime, timezone
272
+ return datetime.now(timezone.utc).isoformat()
273
+
274
+
275
+ def setup_team_sync_env(org_id: str, backend: "StorageBackend" | None = None) -> None:
276
+ """Ensure the process environment points at the cloud backend for this org.
277
+
278
+ This is mainly a convenience for CLI tests; the backend factory is normally
279
+ driven by ``DEVTORCH_STORAGE`` and friends.
280
+ """
281
+ os.environ.setdefault("DEVTORCH_ORG_ID", org_id)
282
+ if backend is None:
283
+ os.environ.setdefault("DEVTORCH_STORAGE", "r2")
@@ -0,0 +1,9 @@
1
+ from devtorch_core.codex.proxy import CodexProxy, install_codex_hook, get_codex_hook_status
2
+ from devtorch_core.codex.capture import CodexCaptureHandler
3
+
4
+ __all__ = [
5
+ "CodexProxy",
6
+ "CodexCaptureHandler",
7
+ "install_codex_hook",
8
+ "get_codex_hook_status",
9
+ ]