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.
- devtorch_core/__init__.py +158 -0
- devtorch_core/aggphi_textual.py +275 -0
- devtorch_core/alerts/__init__.py +23 -0
- devtorch_core/alerts/base.py +46 -0
- devtorch_core/alerts/config.py +60 -0
- devtorch_core/alerts/dispatcher.py +110 -0
- devtorch_core/alerts/jira.py +96 -0
- devtorch_core/alerts/linear.py +72 -0
- devtorch_core/alerts/pagerduty.py +66 -0
- devtorch_core/alerts/slack.py +81 -0
- devtorch_core/alerts/teams.py +70 -0
- devtorch_core/audit/__init__.py +43 -0
- devtorch_core/audit/exporter.py +297 -0
- devtorch_core/audit/privacy.py +101 -0
- devtorch_core/audit/scrubber.py +149 -0
- devtorch_core/audit/service.py +67 -0
- devtorch_core/audit/signing.py +127 -0
- devtorch_core/broadcast/__init__.py +4 -0
- devtorch_core/broadcast/broadcaster.py +100 -0
- devtorch_core/broadcast/watcher.py +71 -0
- devtorch_core/capability.py +639 -0
- devtorch_core/cloud/__init__.py +1 -0
- devtorch_core/cloud/client_config.py +472 -0
- devtorch_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
- devtorch_core/cloud/client_configs/.claude-stdio.json +13 -0
- devtorch_core/cloud/client_configs/.cursor-mcp.json +13 -0
- devtorch_core/cloud/client_configs/.opencode-bridge.json +13 -0
- devtorch_core/cloud/client_configs/.opencode.json +15 -0
- devtorch_core/cloud/client_configs/.vscode-mcp.json +13 -0
- devtorch_core/cloud/devtorch-mcp-bridge.js +357 -0
- devtorch_core/cloud/mcp_client.py +229 -0
- devtorch_core/cloud/setup.py +144 -0
- devtorch_core/cloud/sync.py +143 -0
- devtorch_core/cloud/sync_bundle.py +603 -0
- devtorch_core/cloud/sync_conflicts.py +159 -0
- devtorch_core/cloud/sync_state.py +159 -0
- devtorch_core/cloud/team_sync.py +283 -0
- devtorch_core/codex/__init__.py +9 -0
- devtorch_core/codex/__main__.py +97 -0
- devtorch_core/codex/capture.py +208 -0
- devtorch_core/codex/proxy.py +412 -0
- devtorch_core/concept_catalog.py +209 -0
- devtorch_core/consolidation/__init__.py +3 -0
- devtorch_core/consolidation/synthesizer.py +87 -0
- devtorch_core/consolidation/workflow.py +175 -0
- devtorch_core/daemon/__init__.py +27 -0
- devtorch_core/daemon/supervisor.py +293 -0
- devtorch_core/daemon/watcher.py +244 -0
- devtorch_core/dashboard_api.py +2012 -0
- devtorch_core/deltaf.py +97 -0
- devtorch_core/disclosure.py +50 -0
- devtorch_core/divergence/__init__.py +3 -0
- devtorch_core/divergence/detector.py +166 -0
- devtorch_core/gateway/__init__.py +32 -0
- devtorch_core/gateway/key_manager.py +124 -0
- devtorch_core/gateway/metrics_webhook.py +252 -0
- devtorch_core/gateway/policy.py +262 -0
- devtorch_core/gateway/server.py +727 -0
- devtorch_core/gateway/sso.py +233 -0
- devtorch_core/gcc.py +1246 -0
- devtorch_core/github/__init__.py +35 -0
- devtorch_core/github/app.py +240 -0
- devtorch_core/github/comment_builder.py +113 -0
- devtorch_core/github/pat.py +76 -0
- devtorch_core/github/pr_parser.py +82 -0
- devtorch_core/github/pr_reporter.py +555 -0
- devtorch_core/gitlab/__init__.py +177 -0
- devtorch_core/hitl/__init__.py +4 -0
- devtorch_core/hitl/channels.py +129 -0
- devtorch_core/hitl/orchestrator.py +95 -0
- devtorch_core/hooks/__init__.py +17 -0
- devtorch_core/hooks/claude_code.py +228 -0
- devtorch_core/hooks/git_capture.py +341 -0
- devtorch_core/hooks/git_commit.py +182 -0
- devtorch_core/hooks/installer.py +733 -0
- devtorch_core/hooks/pre_commit.py +157 -0
- devtorch_core/hooks/runner.py +344 -0
- devtorch_core/identity/__init__.py +4 -0
- devtorch_core/identity/agent.py +86 -0
- devtorch_core/identity/providers.py +85 -0
- devtorch_core/invariants.py +182 -0
- devtorch_core/mcp/__init__.py +10 -0
- devtorch_core/mcp/auth.py +177 -0
- devtorch_core/mcp/server.py +1049 -0
- devtorch_core/metrics/__init__.py +35 -0
- devtorch_core/metrics/aggregate.py +215 -0
- devtorch_core/metrics/calibrate.py +198 -0
- devtorch_core/metrics/calibration.py +125 -0
- devtorch_core/metrics/credibility.py +288 -0
- devtorch_core/metrics/delivery_time.py +70 -0
- devtorch_core/metrics/dhs.py +126 -0
- devtorch_core/metrics/mcs.py +96 -0
- devtorch_core/metrics/roi.py +88 -0
- devtorch_core/metrics/session_writer.py +81 -0
- devtorch_core/metrics/shadow_ai.py +117 -0
- devtorch_core/metrics/sprint_writer.py +243 -0
- devtorch_core/observability/__init__.py +78 -0
- devtorch_core/observability/datadog.py +157 -0
- devtorch_core/observability/formatter.py +119 -0
- devtorch_core/observability/report.py +264 -0
- devtorch_core/observability/servicenow.py +147 -0
- devtorch_core/observability/splunk.py +218 -0
- devtorch_core/observability/webhook.py +227 -0
- devtorch_core/parser/__init__.py +30 -0
- devtorch_core/parser/blocks.py +216 -0
- devtorch_core/parser/inference.py +159 -0
- devtorch_core/parser/thinking.py +112 -0
- devtorch_core/projects.py +169 -0
- devtorch_core/prompt_artifact.py +76 -0
- devtorch_core/proxy/__init__.py +9 -0
- devtorch_core/proxy/routes/__init__.py +1 -0
- devtorch_core/proxy/routes/anthropic.py +264 -0
- devtorch_core/proxy/routes/azure_openai.py +336 -0
- devtorch_core/proxy/routes/gemini.py +331 -0
- devtorch_core/proxy/routes/groq.py +284 -0
- devtorch_core/proxy/routes/ollama.py +279 -0
- devtorch_core/proxy/routes/openai.py +287 -0
- devtorch_core/proxy/server.py +356 -0
- devtorch_core/query/__init__.py +15 -0
- devtorch_core/query/grep.py +181 -0
- devtorch_core/query/hybrid.py +86 -0
- devtorch_core/query/semantic.py +157 -0
- devtorch_core/rdp.py +105 -0
- devtorch_core/reasoning/__init__.py +4 -0
- devtorch_core/reasoning/entry.py +31 -0
- devtorch_core/reasoning/store.py +122 -0
- devtorch_core/reasoning_plus/__init__.py +70 -0
- devtorch_core/reasoning_plus/augmenter.py +326 -0
- devtorch_core/reasoning_plus/capture.py +51 -0
- devtorch_core/reasoning_plus/config.py +256 -0
- devtorch_core/reasoning_plus/context.py +262 -0
- devtorch_core/reasoning_plus/learning/__init__.py +72 -0
- devtorch_core/reasoning_plus/learning/analytics.py +141 -0
- devtorch_core/reasoning_plus/learning/api.py +313 -0
- devtorch_core/reasoning_plus/learning/chain.py +285 -0
- devtorch_core/reasoning_plus/learning/composer.py +74 -0
- devtorch_core/reasoning_plus/learning/cross_project.py +234 -0
- devtorch_core/reasoning_plus/learning/embeddings.py +209 -0
- devtorch_core/reasoning_plus/learning/extractor.py +207 -0
- devtorch_core/reasoning_plus/learning/models.py +116 -0
- devtorch_core/reasoning_plus/learning/provenance.py +126 -0
- devtorch_core/reasoning_plus/learning/recorder.py +81 -0
- devtorch_core/reasoning_plus/learning/relevance.py +122 -0
- devtorch_core/reasoning_plus/learning/state.py +86 -0
- devtorch_core/reasoning_plus/learning/store.py +160 -0
- devtorch_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
- devtorch_core/reasoning_plus/prompt.py +90 -0
- devtorch_core/rep.py +134 -0
- devtorch_core/rep_network/__init__.py +25 -0
- devtorch_core/rep_network/merge.py +70 -0
- devtorch_core/rep_network/node.py +137 -0
- devtorch_core/rep_network/server.py +140 -0
- devtorch_core/rep_network/sync.py +207 -0
- devtorch_core/sensitivity.py +182 -0
- devtorch_core/serve.py +258 -0
- devtorch_core/session/__init__.py +39 -0
- devtorch_core/session/disagreement.py +188 -0
- devtorch_core/session/models.py +114 -0
- devtorch_core/session/orchestrator.py +182 -0
- devtorch_core/session/planner.py +169 -0
- devtorch_core/session/simulator.py +132 -0
- devtorch_core/signing.py +290 -0
- devtorch_core/sis.py +197 -0
- devtorch_core/storage.py +308 -0
- devtorch_core/templates/__init__.py +6 -0
- devtorch_core/templates/engine.py +122 -0
- devtorch_core/templates/go.py +18 -0
- devtorch_core/templates/infra.py +19 -0
- devtorch_core/templates/library/__init__.py +18 -0
- devtorch_core/templates/library/api_design.md +27 -0
- devtorch_core/templates/library/bug_fix.md +27 -0
- devtorch_core/templates/library/decision_record.md +27 -0
- devtorch_core/templates/library/engine.py +228 -0
- devtorch_core/templates/library/security_review.md +30 -0
- devtorch_core/templates/python.py +19 -0
- devtorch_core/templates/react.py +18 -0
- devtorch_core/templates/typescript.py +18 -0
- devtorch_core/theta.py +221 -0
- devtorch_core/theta_synthesis.py +268 -0
- devtorch_core/topics.py +320 -0
- devtorch_core/variance.py +219 -0
- devtorch_core/wrapper/__init__.py +52 -0
- devtorch_core/wrapper/anthropic.py +487 -0
- devtorch_core/wrapper/base.py +562 -0
- devtorch_core/wrapper/bedrock.py +342 -0
- devtorch_core/wrapper/gemini.py +422 -0
- devtorch_core/wrapper/ollama.py +527 -0
- devtorch_core/wrapper/openai.py +461 -0
- devtorch_core-3.0.1.dist-info/METADATA +867 -0
- devtorch_core-3.0.1.dist-info/RECORD +193 -0
- devtorch_core-3.0.1.dist-info/WHEEL +5 -0
- devtorch_core-3.0.1.dist-info/entry_points.txt +2 -0
- 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
|
+
]
|