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.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 @@
|
|
|
1
|
+
# devtorch_core.proxy.routes
|