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.parser.inference
3
+ ================================
4
+ Heuristic inference of sensitivity signals and decision summaries from raw LLM
5
+ response text.
6
+
7
+ This is the *fallback* path used when the LLM did not emit explicit RACP
8
+ ``<devtorch:sensitivity>`` or ``<devtorch:commit>`` blocks. All inferred
9
+ results carry ``confidence=0.5`` to distinguish them from LLM-asserted values.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ from dataclasses import dataclass, field
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # Data class
18
+ # ---------------------------------------------------------------------------
19
+
20
+ @dataclass
21
+ class InferredSensitivity:
22
+ concept: str
23
+ signal: str
24
+ confidence: float = 0.5
25
+ source: str = "inference_heuristic"
26
+
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Keyword → concept mapping
30
+ # ---------------------------------------------------------------------------
31
+
32
+ # Each entry is (compiled_pattern, concept_label).
33
+ _SENSITIVITY_PATTERNS: list[tuple[re.Pattern[str], str]] = [
34
+ (re.compile(r'\b(schema|migration|database)\b', re.IGNORECASE), 'data_schema'),
35
+ (re.compile(r'\b(auth|authentication|token|jwt|session)\b', re.IGNORECASE), 'authentication'),
36
+ (re.compile(r'\b(api|endpoint|route|url)\b', re.IGNORECASE), 'api_interface'),
37
+ (re.compile(r'\b(deploy|release|rollout|pipeline)\b', re.IGNORECASE), 'deployment'),
38
+ (re.compile(r'\b(security|vulnerability|cve|injection)\b', re.IGNORECASE), 'security'),
39
+ (re.compile(r'\b(performance|latency|throughput|cache)\b', re.IGNORECASE), 'performance'),
40
+ (re.compile(r'\b(config|configuration|environment|env)\b', re.IGNORECASE), 'configuration'),
41
+ ]
42
+
43
+ # Sentence boundary: split on . ! ? followed by whitespace or end-of-string.
44
+ _RE_SENTENCE_SPLIT = re.compile(r'(?<=[.!?])\s+')
45
+
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Helpers
49
+ # ---------------------------------------------------------------------------
50
+
51
+ def _sentences(text: str) -> list[str]:
52
+ """Split text into rough sentences."""
53
+ return [s.strip() for s in _RE_SENTENCE_SPLIT.split(text) if s.strip()]
54
+
55
+
56
+ def _find_surrounding_sentence(text: str, match: re.Match[str]) -> str:
57
+ """
58
+ Return the sentence that contains the regex match, truncated to 120 chars.
59
+ Falls back to the matched substring itself if no sentence boundary is found.
60
+ """
61
+ start = match.start()
62
+ # Walk backwards to the previous sentence boundary (. ! ? or start of text).
63
+ before = text[:start]
64
+ sentence_start = max(
65
+ before.rfind("."),
66
+ before.rfind("!"),
67
+ before.rfind("?"),
68
+ )
69
+ sentence_start = sentence_start + 1 if sentence_start != -1 else 0
70
+
71
+ # Walk forwards to the next sentence boundary.
72
+ after = text[start:]
73
+ ends = [after.find(c) for c in ".!?" if after.find(c) != -1]
74
+ sentence_end = start + min(ends) + 1 if ends else len(text)
75
+
76
+ sentence = text[sentence_start:sentence_end].strip()
77
+ return sentence[:120]
78
+
79
+
80
+ # ---------------------------------------------------------------------------
81
+ # Public API
82
+ # ---------------------------------------------------------------------------
83
+
84
+ def infer_sensitivity(text: str, max_concepts: int = 2) -> list[InferredSensitivity]:
85
+ """
86
+ Heuristically infer sensitivity signals from response text.
87
+
88
+ Scans *text* for keyword patterns defined in ``_SENSITIVITY_PATTERNS``.
89
+ Returns up to *max_concepts* :class:`InferredSensitivity` objects.
90
+
91
+ All results have ``confidence=0.5`` — this is the fallback path only.
92
+ Signal text is the first 120 characters of the surrounding sentence.
93
+ """
94
+ if not text or not text.strip():
95
+ return []
96
+
97
+ results: list[InferredSensitivity] = []
98
+ seen_concepts: set[str] = set()
99
+
100
+ for pattern, concept in _SENSITIVITY_PATTERNS:
101
+ if len(results) >= max_concepts:
102
+ break
103
+ if concept in seen_concepts:
104
+ continue
105
+ match = pattern.search(text)
106
+ if match:
107
+ signal = _find_surrounding_sentence(text, match)
108
+ results.append(
109
+ InferredSensitivity(
110
+ concept=concept,
111
+ signal=signal,
112
+ )
113
+ )
114
+ seen_concepts.add(concept)
115
+
116
+ return results
117
+
118
+
119
+ def extract_decision_summary(text: str, max_chars: int = 150) -> str:
120
+ """
121
+ Extract a short decision summary from the first meaningful sentence of text.
122
+
123
+ Processing steps:
124
+
125
+ 1. Strip markdown formatting (headings, bold, italic, code fences,
126
+ inline code, bullet markers).
127
+ 2. Find the first non-empty sentence.
128
+ 3. Truncate to *max_chars*.
129
+ 4. Prefix with ``"auto: "``.
130
+
131
+ Returns ``"auto: (no content)"`` if *text* is empty or blank.
132
+ """
133
+ if not text or not text.strip():
134
+ return "auto: (no content)"
135
+
136
+ # Remove fenced code blocks.
137
+ cleaned = re.sub(r"```[\s\S]*?```", " ", text)
138
+ # Remove inline code.
139
+ cleaned = re.sub(r"`[^`]*`", " ", cleaned)
140
+ # Remove markdown headings.
141
+ cleaned = re.sub(r"^#{1,6}\s+", "", cleaned, flags=re.MULTILINE)
142
+ # Remove bold/italic markers (**text**, __text__, *text*, _text_).
143
+ cleaned = re.sub(r"\*{1,3}([^*]+)\*{1,3}", r"\1", cleaned)
144
+ cleaned = re.sub(r"_{1,3}([^_]+)_{1,3}", r"\1", cleaned)
145
+ # Remove bullet / list markers.
146
+ cleaned = re.sub(r"^\s*[-*+]\s+", "", cleaned, flags=re.MULTILINE)
147
+ cleaned = re.sub(r"^\s*\d+\.\s+", "", cleaned, flags=re.MULTILINE)
148
+ # Collapse whitespace.
149
+ cleaned = " ".join(cleaned.split())
150
+
151
+ # Extract first meaningful sentence.
152
+ sentences = _RE_SENTENCE_SPLIT.split(cleaned)
153
+ first = next((s.strip() for s in sentences if s.strip()), "")
154
+
155
+ if not first:
156
+ return "auto: (no content)"
157
+
158
+ truncated = first[:max_chars].rstrip()
159
+ return f"auto: {truncated}"
@@ -0,0 +1,112 @@
1
+ """
2
+ devtorch_core.parser.thinking
3
+ ==============================
4
+ Normalise extended-thinking / reasoning content blocks returned by LLM APIs.
5
+
6
+ Supported formats
7
+ -----------------
8
+ Anthropic extended thinking
9
+ ``response["content"]`` is a list of content blocks. Blocks whose
10
+ ``"type"`` equals ``"thinking"`` carry the reasoning text in
11
+ ``block["thinking"]``.
12
+
13
+ OpenAI o1 / o3 reasoning
14
+ ``response["choices"][0]["message"]["reasoning_content"]`` is a string
15
+ containing the chain-of-thought the model produced before answering.
16
+
17
+ Standard models
18
+ Neither format is present. An empty list is returned.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import logging
23
+ from dataclasses import dataclass, field
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Data class
30
+ # ---------------------------------------------------------------------------
31
+
32
+ @dataclass
33
+ class ThinkingBlock:
34
+ text: str
35
+ confidence: float = 1.0 # thinking tokens always conf=1.0
36
+ source: str = "thinking_content"
37
+
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # Public API
41
+ # ---------------------------------------------------------------------------
42
+
43
+ def extract_thinking_blocks(response: dict) -> list[ThinkingBlock]:
44
+ """
45
+ Extract thinking / reasoning content from an LLM API response dict.
46
+
47
+ Anthropic format
48
+ ``response["content"]`` is a list; items with ``type == "thinking"``
49
+ have a ``"thinking"`` key with the text.
50
+
51
+ OpenAI o1/o3 format
52
+ ``response["choices"][0]["message"]["reasoning_content"]`` is a string.
53
+
54
+ Returns a list of :class:`ThinkingBlock` objects (may be empty for
55
+ standard non-reasoning models).
56
+ """
57
+ blocks: list[ThinkingBlock] = []
58
+
59
+ # --- Anthropic format ---
60
+ content = response.get("content")
61
+ if isinstance(content, list):
62
+ for item in content:
63
+ if not isinstance(item, dict):
64
+ continue
65
+ if item.get("type") == "thinking":
66
+ text = item.get("thinking", "")
67
+ if text:
68
+ blocks.append(ThinkingBlock(text=str(text)))
69
+ # If we found Anthropic-style blocks, return early — no need to also
70
+ # scan OpenAI format.
71
+ if blocks:
72
+ return blocks
73
+
74
+ # --- OpenAI o1/o3 format ---
75
+ try:
76
+ choices = response.get("choices")
77
+ if isinstance(choices, list) and choices:
78
+ message = choices[0].get("message", {})
79
+ reasoning = message.get("reasoning_content")
80
+ if reasoning and isinstance(reasoning, str):
81
+ blocks.append(
82
+ ThinkingBlock(
83
+ text=reasoning,
84
+ source="reasoning_content",
85
+ )
86
+ )
87
+ except (AttributeError, IndexError, KeyError) as exc:
88
+ logger.warning("devtorch parser: error reading OpenAI reasoning_content — %s", exc)
89
+
90
+ return blocks
91
+
92
+
93
+ def thinking_to_commit_message(
94
+ blocks: list[ThinkingBlock],
95
+ max_chars: int = 200,
96
+ ) -> str:
97
+ """
98
+ Summarise thinking blocks into a short commit message.
99
+
100
+ Takes the first *max_chars* characters of the first block's text (newlines
101
+ stripped/collapsed). Returns a string prefixed with ``"auto: "``.
102
+
103
+ If *blocks* is empty, returns ``"auto: (no thinking content)"``.
104
+ """
105
+ if not blocks:
106
+ return "auto: (no thinking content)"
107
+
108
+ text = blocks[0].text
109
+ # Collapse whitespace and newlines into single spaces.
110
+ text = " ".join(text.split())
111
+ truncated = text[:max_chars].rstrip()
112
+ return f"auto: {truncated}"
@@ -0,0 +1,169 @@
1
+ """
2
+ Sprint 19 — Multi-project registry.
3
+
4
+ Maintains a machine-level index of all DevTorch-managed repos at
5
+ ~/.devtorch/registry.json. Each entry records:
6
+ - name
7
+ - path (resolved absolute path)
8
+ - developer_id
9
+ - added_at
10
+ - last_synced
11
+
12
+ API is intentionally simple: the registry is metadata-only; reasoning state
13
+ remains in each project's isolated .GCC/ directory.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import subprocess
20
+ from dataclasses import dataclass, asdict
21
+ from datetime import datetime, timezone
22
+ from pathlib import Path
23
+ from typing import Dict, List, Optional
24
+
25
+
26
+ DEFAULT_REGISTRY_DIR = Path.home() / ".devtorch"
27
+ DEFAULT_REGISTRY_FILE = "registry.json"
28
+ REGISTRY_PATH_ENV = "DEVTORCH_REGISTRY_PATH"
29
+
30
+
31
+ @dataclass
32
+ class ProjectEntry:
33
+ name: str
34
+ path: str
35
+ developer_id: str
36
+ added_at: str
37
+ last_synced: str
38
+
39
+
40
+ class ProjectRegistry:
41
+ """Machine-level registry of DevTorch-managed projects."""
42
+
43
+ def __init__(self, registry_path: Optional[Path] = None) -> None:
44
+ if registry_path is None:
45
+ env_path = os.environ.get(REGISTRY_PATH_ENV, "").strip()
46
+ if env_path:
47
+ registry_path = Path(env_path)
48
+ else:
49
+ registry_path = DEFAULT_REGISTRY_DIR / DEFAULT_REGISTRY_FILE
50
+ self.registry_path = Path(registry_path)
51
+ self._ensure_dir()
52
+
53
+ def _ensure_dir(self) -> None:
54
+ self.registry_path.parent.mkdir(parents=True, exist_ok=True)
55
+
56
+ def _read(self) -> dict:
57
+ if not self.registry_path.exists():
58
+ return {"version": "1", "projects": []}
59
+ try:
60
+ data = json.loads(self.registry_path.read_text(encoding="utf-8"))
61
+ if not isinstance(data, dict):
62
+ return {"version": "1", "projects": []}
63
+ return data
64
+ except (json.JSONDecodeError, OSError):
65
+ return {"version": "1", "projects": []}
66
+
67
+ def _write(self, data: dict) -> None:
68
+ self._ensure_dir()
69
+ self.registry_path.write_text(json.dumps(data, indent=2, sort_keys=True), encoding="utf-8")
70
+
71
+ def _guess_developer_id(self, project_path: Path) -> str:
72
+ """Guess developer_id from git config user.email in the project."""
73
+ try:
74
+ result = subprocess.run(
75
+ ["git", "-C", str(project_path), "config", "user.email"],
76
+ capture_output=True,
77
+ text=True,
78
+ timeout=2,
79
+ check=False,
80
+ )
81
+ email = result.stdout.strip()
82
+ if email:
83
+ return email
84
+ except Exception:
85
+ pass
86
+ env_id = os.environ.get("DEVTORCH_DEVELOPER_ID", "").strip()
87
+ if env_id:
88
+ return env_id
89
+ return "unknown"
90
+
91
+ def list(self) -> List[ProjectEntry]:
92
+ data = self._read()
93
+ entries = []
94
+ for raw in data.get("projects", []):
95
+ try:
96
+ entries.append(ProjectEntry(**raw))
97
+ except (TypeError, ValueError):
98
+ continue
99
+ return entries
100
+
101
+ def add(
102
+ self,
103
+ project_path: Path,
104
+ name: Optional[str] = None,
105
+ developer_id: Optional[str] = None,
106
+ ) -> ProjectEntry:
107
+ """Add a project to the registry. Idempotent by resolved path."""
108
+ project_path = Path(project_path).resolve()
109
+ if not project_path.exists():
110
+ raise ValueError(f"Project path does not exist: {project_path}")
111
+
112
+ data = self._read()
113
+ projects = data.get("projects", [])
114
+
115
+ resolved = str(project_path)
116
+ for raw in projects:
117
+ if Path(raw["path"]).resolve() == project_path:
118
+ # Update existing entry
119
+ raw["name"] = name or raw.get("name") or project_path.name
120
+ raw["developer_id"] = developer_id or raw.get("developer_id") or self._guess_developer_id(project_path)
121
+ raw["last_synced"] = _now_iso()
122
+ self._write(data)
123
+ return ProjectEntry(**raw)
124
+
125
+ entry = ProjectEntry(
126
+ name=name or project_path.name,
127
+ path=resolved,
128
+ developer_id=developer_id or self._guess_developer_id(project_path),
129
+ added_at=_now_iso(),
130
+ last_synced=_now_iso(),
131
+ )
132
+ projects.append(asdict(entry))
133
+ data["projects"] = projects
134
+ self._write(data)
135
+ return entry
136
+
137
+ def remove(self, project_path: Path) -> bool:
138
+ """Remove a project by path. Returns True if removed."""
139
+ project_path = Path(project_path).resolve()
140
+ data = self._read()
141
+ projects = data.get("projects", [])
142
+ original_len = len(projects)
143
+ projects = [
144
+ raw for raw in projects
145
+ if Path(raw["path"]).resolve() != project_path
146
+ ]
147
+ if len(projects) == original_len:
148
+ return False
149
+ data["projects"] = projects
150
+ self._write(data)
151
+ return True
152
+
153
+ def sync(self) -> List[ProjectEntry]:
154
+ """Re-read each registered project and update last_synced."""
155
+ data = self._read()
156
+ projects = data.get("projects", [])
157
+ updated = []
158
+ for raw in projects:
159
+ path = Path(raw["path"])
160
+ if path.exists():
161
+ raw["last_synced"] = _now_iso()
162
+ updated.append(raw)
163
+ data["projects"] = updated
164
+ self._write(data)
165
+ return [ProjectEntry(**raw) for raw in updated]
166
+
167
+
168
+ def _now_iso() -> str:
169
+ return datetime.now(timezone.utc).isoformat()
@@ -0,0 +1,76 @@
1
+ """
2
+ Sprint 5 – A3: Canonical system prompt artifact RACP_SYSTEM_PROMPT_v2.1.
3
+
4
+ Stored under .GCC/ and referenced by Textual Mode runs.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ PROMPTS_DIR_NAME = "prompts"
14
+ RACP_SYSTEM_PROMPT_NAME = "RACP_SYSTEM_PROMPT_v2.1.md"
15
+ RACP_SYSTEM_PROMPT_META = "RACP_SYSTEM_PROMPT_v2.1_meta.json"
16
+
17
+ DEFAULT_PROMPT_CONTENT = """# RACP System Prompt v2.1
18
+
19
+ You are a reasoning agent operating under the RACP (Reasoning and Coordination Protocol) v2.1.
20
+
21
+ When you make a reasoning decision, emit a self-closing XML tag inside your
22
+ response text so DevTorch can capture it:
23
+
24
+ <devtorch:commit message="why you chose this approach and what
25
+ alternatives you considered" concepts="comma,separated,concept,names"
26
+ confidence="0.85"/>
27
+
28
+ If the decision touches a sensitive area (auth, schema, payments, secrets, PII,
29
+ external APIs, security, or config), also emit:
30
+
31
+ <devtorch:sensitivity concept="area_name"
32
+ signal="what this decision depends on or could affect"
33
+ confidence="0.9" disclosure="PROTECTED"/>
34
+
35
+ The disclosure attribute must be one of: PUBLIC, PROTECTED, or PRIVATE.
36
+
37
+ When citing the coordination vector, refer to concepts defined in
38
+ [DevTorch Θ — top concepts]. Respect any invariant constraints (I1, I3).
39
+ Do not leak [PRIVATE] content into reasoning messages.
40
+ """
41
+
42
+
43
+ class PromptArtifactStore:
44
+ """Store and retrieve the canonical RACP_SYSTEM_PROMPT_v2.1 artifact."""
45
+ def __init__(self, gcc_dir: Path) -> None:
46
+ self.prompts_dir = gcc_dir / PROMPTS_DIR_NAME
47
+ self.prompt_path = self.prompts_dir / RACP_SYSTEM_PROMPT_NAME
48
+ self.meta_path = self.prompts_dir / RACP_SYSTEM_PROMPT_META
49
+
50
+ def ensure_dir(self) -> None:
51
+ self.prompts_dir.mkdir(parents=True, exist_ok=True)
52
+
53
+ def set_prompt(self, content: str, version: str = "v2.1") -> None:
54
+ """Store prompt text and metadata."""
55
+ self.ensure_dir()
56
+ self.prompt_path.write_text(content, encoding="utf-8")
57
+ meta = {"version": version, "artifact": RACP_SYSTEM_PROMPT_NAME}
58
+ self.meta_path.write_text(json.dumps(meta, indent=2) + "\n", encoding="utf-8")
59
+
60
+ def get_prompt(self) -> Optional[str]:
61
+ """Return prompt content or None if not set."""
62
+ if not self.prompt_path.exists():
63
+ return None
64
+ return self.prompt_path.read_text(encoding="utf-8")
65
+
66
+ def get_meta(self) -> Optional[dict]:
67
+ """Return metadata dict or None."""
68
+ if not self.meta_path.exists():
69
+ return None
70
+ return json.loads(self.meta_path.read_text(encoding="utf-8"))
71
+
72
+ def reference_for_textual_mode(self) -> Optional[str]:
73
+ """Return path reference for Textual Mode runs (relative to .GCC)."""
74
+ if not self.prompt_path.exists():
75
+ return None
76
+ return f"{PROMPTS_DIR_NAME}/{RACP_SYSTEM_PROMPT_NAME}"
@@ -0,0 +1,9 @@
1
+ from .server import create_app, run_proxy, install_service, DEFAULT_PORT, DEFAULT_HOST
2
+
3
+ __all__ = [
4
+ "create_app",
5
+ "run_proxy",
6
+ "install_service",
7
+ "DEFAULT_PORT",
8
+ "DEFAULT_HOST",
9
+ ]
@@ -0,0 +1 @@
1
+ # devtorch_core.proxy.routes