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,639 @@
1
+ """
2
+ Sprint 3 – Ωᵢ capability declaration (v2.1 A1 fields) + PST runner.
3
+
4
+ OmegaCapability is the per-agent capability record that must be set and valid
5
+ before a node may participate in Textual Mode. It captures the empirical
6
+ Lipschitz bound (L̂), calibration metadata, and PST (Prompt Stability Test)
7
+ results. CapabilityStore handles on-disk persistence, and PSTRunner provides
8
+ the evaluation harness for measuring output stability.
9
+
10
+ A1 Textual Mode gate (RACP spec v2.1):
11
+ Textual Mode is allowed iff ALL of the following hold:
12
+ 1. A valid Ωᵢ exists (not None / not stub format).
13
+ 2. Calibration date is within expiry_days of today.
14
+ 3. L̂ ≤ L_max (configurable; default = 2.0).
15
+ 4. pst_status == "passed".
16
+ If any condition fails a CAPABILITY_DEGRADED event is emitted by the
17
+ GCCRepository layer, and the agent MUST fall back to Numerical-only /
18
+ deterministic mode.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import dataclasses
24
+ import datetime as _dt
25
+ import json
26
+ from collections import Counter
27
+ from pathlib import Path
28
+ from typing import Callable, List, Optional
29
+
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Constants
33
+ # ---------------------------------------------------------------------------
34
+
35
+ EXPIRY_DAYS_DEFAULT: int = 90 # Calibration expires after 90 days
36
+ L_MAX_DEFAULT: float = 2.0 # Default Lipschitz bound threshold
37
+ PST_SCORE_THRESHOLD: float = 0.80 # PST must score ≥ 80 % to pass
38
+
39
+ PST_STATUS_PASSED = "passed"
40
+ PST_STATUS_FAILED = "failed"
41
+ PST_STATUS_NOT_RUN = "not_run"
42
+
43
+ CAPABILITY_DEGRADED_EVENT = "CAPABILITY_DEGRADED"
44
+ OMEGA_SCHEMA_VERSION = "omega-v2.1"
45
+
46
+ ROLE_POLICY_FILENAME = "role_policy.json"
47
+ ROLES_SUBDIR = "roles"
48
+
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # AgentRole dataclass
52
+ # ---------------------------------------------------------------------------
53
+
54
+ @dataclasses.dataclass
55
+ class AgentRole:
56
+ """
57
+ Role-based policy for agent capabilities and concept access.
58
+
59
+ A role constrains what an agent may do based on its OmegaCapability:
60
+ - allowed_concepts: concept names the role may touch (empty means all).
61
+ - disallowed_concepts: concept names the role may never touch.
62
+ - required_capabilities: OmegaCapability field names that must be present.
63
+ - max_temperature: optional sampling-temperature ceiling.
64
+ """
65
+
66
+ role_id: str
67
+ name: str
68
+ allowed_concepts: List[str]
69
+ disallowed_concepts: List[str]
70
+ required_capabilities: List[str]
71
+ max_temperature: Optional[float] = None
72
+ created_at: str = dataclasses.field(
73
+ default_factory=lambda: _dt.datetime.now(tz=_dt.timezone.utc).isoformat()
74
+ )
75
+ updated_at: str = dataclasses.field(
76
+ default_factory=lambda: _dt.datetime.now(tz=_dt.timezone.utc).isoformat()
77
+ )
78
+
79
+ def __post_init__(self) -> None:
80
+ if not self.role_id.strip():
81
+ raise ValueError("role_id must not be empty.")
82
+ if not self.name.strip():
83
+ raise ValueError("name must not be empty.")
84
+ if self.max_temperature is not None and not (0.0 <= self.max_temperature <= 2.0):
85
+ raise ValueError("max_temperature must be in [0.0, 2.0].")
86
+
87
+ def to_dict(self) -> dict:
88
+ return dataclasses.asdict(self)
89
+
90
+ @classmethod
91
+ def from_dict(cls, d: dict) -> "AgentRole":
92
+ known = {f.name for f in dataclasses.fields(cls)}
93
+ return cls(**{k: v for k, v in d.items() if k in known})
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # RolePolicy dataclass
98
+ # ---------------------------------------------------------------------------
99
+
100
+ @dataclasses.dataclass
101
+ class RolePolicy:
102
+ """Collection of AgentRole objects with a default_role identifier."""
103
+
104
+ roles: List[AgentRole]
105
+ default_role: str
106
+ created_at: str = dataclasses.field(
107
+ default_factory=lambda: _dt.datetime.now(tz=_dt.timezone.utc).isoformat()
108
+ )
109
+ updated_at: str = dataclasses.field(
110
+ default_factory=lambda: _dt.datetime.now(tz=_dt.timezone.utc).isoformat()
111
+ )
112
+
113
+ def __post_init__(self) -> None:
114
+ if not self.default_role.strip():
115
+ raise ValueError("default_role must not be empty.")
116
+ role_ids = {r.role_id for r in self.roles}
117
+ if self.default_role not in role_ids:
118
+ raise ValueError(
119
+ f"default_role '{self.default_role}' is not in roles {role_ids}."
120
+ )
121
+ if len(role_ids) != len(self.roles):
122
+ raise ValueError("role_ids must be unique within a RolePolicy.")
123
+
124
+ def to_dict(self) -> dict:
125
+ return {
126
+ "roles": [r.to_dict() for r in self.roles],
127
+ "default_role": self.default_role,
128
+ "created_at": self.created_at,
129
+ "updated_at": self.updated_at,
130
+ }
131
+
132
+ @classmethod
133
+ def from_dict(cls, d: dict) -> "RolePolicy":
134
+ roles = [AgentRole.from_dict(r) for r in d.get("roles", [])]
135
+ known = {f.name for f in dataclasses.fields(cls)}
136
+ return cls(
137
+ roles=roles,
138
+ default_role=d.get("default_role", ""),
139
+ **{k: v for k, v in d.items() if k in known and k not in ("roles", "default_role")},
140
+ )
141
+
142
+ def get_role(self, role_id: str) -> Optional[AgentRole]:
143
+ for role in self.roles:
144
+ if role.role_id == role_id:
145
+ return role
146
+ return None
147
+
148
+
149
+ # ---------------------------------------------------------------------------
150
+ # OmegaCapability dataclass
151
+ # ---------------------------------------------------------------------------
152
+
153
+ @dataclasses.dataclass
154
+ class OmegaCapability:
155
+ """
156
+ Per-agent capability declaration (Ωᵢ) for RACP v2.1 A1 compliance.
157
+
158
+ Required A1 fields
159
+ ------------------
160
+ agent_id : Unique agent identifier.
161
+ lipschitz_bound : Empirical L̂ from calibration (must be ≤ L_max).
162
+ calibration_dataset_hash: SHA-256 hex digest of the calibration corpus.
163
+ embedding_model : Name/version of the embedding model used.
164
+ calibration_date : ISO-8601 date (YYYY-MM-DD) of the calibration run.
165
+ temperature_used : Sampling temperature used during calibration [0, 2].
166
+
167
+ Optional A1 fields
168
+ ------------------
169
+ lipschitz_p95 : 95th-percentile Lipschitz estimate.
170
+
171
+ PST fields (populated by PSTRunner)
172
+ ------------------------------------
173
+ pst_status : "passed" | "failed" | "not_run".
174
+ pst_score : Mean stability score [0, 1].
175
+ pst_n_prompts : Number of prompts used.
176
+ pst_n_repetitions : Number of repetitions per prompt.
177
+
178
+ Lifecycle
179
+ ---------
180
+ expiry_days : Days after calibration_date before the declaration expires.
181
+ schema_version : Fixed to "omega-v2.1".
182
+ created_at / updated_at : ISO-8601 UTC timestamps.
183
+ """
184
+
185
+ # Core A1 fields (required)
186
+ agent_id: str
187
+ lipschitz_bound: float
188
+ calibration_dataset_hash: str
189
+ embedding_model: str
190
+ calibration_date: str
191
+ temperature_used: float
192
+
193
+ # Optional A1 field
194
+ lipschitz_p95: Optional[float] = None
195
+
196
+ # PST results
197
+ pst_status: str = PST_STATUS_NOT_RUN
198
+ pst_score: Optional[float] = None
199
+ pst_n_prompts: int = 0
200
+ pst_n_repetitions: int = 0
201
+
202
+ # Lifecycle
203
+ expiry_days: int = EXPIRY_DAYS_DEFAULT
204
+
205
+ # Metadata
206
+ schema_version: str = OMEGA_SCHEMA_VERSION
207
+ created_at: str = dataclasses.field(
208
+ default_factory=lambda: _dt.datetime.now(tz=_dt.timezone.utc).isoformat()
209
+ )
210
+ updated_at: str = dataclasses.field(
211
+ default_factory=lambda: _dt.datetime.now(tz=_dt.timezone.utc).isoformat()
212
+ )
213
+
214
+ def __post_init__(self) -> None:
215
+ if self.pst_status not in (PST_STATUS_PASSED, PST_STATUS_FAILED, PST_STATUS_NOT_RUN):
216
+ raise ValueError(
217
+ f"pst_status must be 'passed', 'failed', or 'not_run', "
218
+ f"got '{self.pst_status}'"
219
+ )
220
+ if self.lipschitz_bound < 0:
221
+ raise ValueError("lipschitz_bound must be non-negative.")
222
+ if not (0.0 <= self.temperature_used <= 2.0):
223
+ raise ValueError("temperature_used must be in [0.0, 2.0].")
224
+ if not self.agent_id.strip():
225
+ raise ValueError("agent_id must not be empty.")
226
+
227
+ def to_dict(self) -> dict:
228
+ return dataclasses.asdict(self)
229
+
230
+ @classmethod
231
+ def from_dict(cls, d: dict) -> "OmegaCapability":
232
+ known = {f.name for f in dataclasses.fields(cls)}
233
+ return cls(**{k: v for k, v in d.items() if k in known})
234
+
235
+ # ------------------------------------------------------------------
236
+ # A1 gate logic
237
+ # ------------------------------------------------------------------
238
+
239
+ def is_expired(self) -> bool:
240
+ """True if the calibration_date is older than expiry_days from today."""
241
+ try:
242
+ cal = _dt.date.fromisoformat(self.calibration_date[:10])
243
+ except (ValueError, TypeError):
244
+ return True # Unparseable → treat as expired
245
+ return (_dt.date.today() - cal).days > self.expiry_days
246
+
247
+ def textual_mode_allowed(
248
+ self, l_max: float = L_MAX_DEFAULT
249
+ ) -> tuple[bool, List[str]]:
250
+ """
251
+ Return (allowed, [failure_reasons]).
252
+
253
+ All three conditions must hold for Textual Mode to be permitted:
254
+ - Calibration not expired.
255
+ - L̂ ≤ L_max.
256
+ - PST status is "passed".
257
+ """
258
+ reasons: List[str] = []
259
+ if self.is_expired():
260
+ reasons.append(
261
+ f"Calibration expired "
262
+ f"(calibration_date={self.calibration_date}, expiry_days={self.expiry_days})."
263
+ )
264
+ if self.lipschitz_bound > l_max:
265
+ reasons.append(
266
+ f"L\u0302={self.lipschitz_bound} exceeds L_max={l_max}."
267
+ )
268
+ if self.pst_status != PST_STATUS_PASSED:
269
+ reasons.append(
270
+ f"PST status is '{self.pst_status}' (must be '{PST_STATUS_PASSED}')."
271
+ )
272
+ return len(reasons) == 0, reasons
273
+
274
+
275
+ # ---------------------------------------------------------------------------
276
+ # CapabilityValidationResult
277
+ # ---------------------------------------------------------------------------
278
+
279
+ @dataclasses.dataclass
280
+ class CapabilityValidationResult:
281
+ """Summary returned by CapabilityStore.validate()."""
282
+
283
+ valid: bool
284
+ textual_mode_allowed: bool
285
+ reasons: List[str]
286
+ agent_id: str
287
+ pst_status: str
288
+ lipschitz_bound: float # float("inf") if no capability exists
289
+ is_expired: bool
290
+
291
+
292
+ # ---------------------------------------------------------------------------
293
+ # CapabilityStore
294
+ # ---------------------------------------------------------------------------
295
+
296
+ class CapabilityStore:
297
+ """
298
+ Reads and writes OmegaCapability to `.GCC/capabilities/omega.json`.
299
+
300
+ The legacy stub format (`schema == "omega-stub-v0"`) is recognised but
301
+ treated as "no capability set", so callers get a clear signal to run
302
+ `devtorch capability set` before proceeding.
303
+ """
304
+
305
+ def __init__(self, capabilities_dir: Path) -> None:
306
+ self.dir = capabilities_dir
307
+ self.omega_path = capabilities_dir / "omega.json"
308
+ self.roles_dir = capabilities_dir / ROLES_SUBDIR
309
+ self.policy_path = capabilities_dir / ROLE_POLICY_FILENAME
310
+
311
+ def save(self, cap: OmegaCapability) -> None:
312
+ self.dir.mkdir(parents=True, exist_ok=True)
313
+ self.omega_path.write_text(
314
+ json.dumps(cap.to_dict(), indent=2, sort_keys=True) + "\n",
315
+ encoding="utf-8",
316
+ )
317
+
318
+ def load(self) -> Optional[OmegaCapability]:
319
+ if not self.omega_path.exists():
320
+ return None
321
+ try:
322
+ data = json.loads(self.omega_path.read_text(encoding="utf-8"))
323
+ except (json.JSONDecodeError, OSError):
324
+ return None
325
+ # Detect legacy stub format from Sprint 0
326
+ if data.get("schema") == "omega-stub-v0":
327
+ return None
328
+ try:
329
+ return OmegaCapability.from_dict(data)
330
+ except (TypeError, ValueError):
331
+ return None
332
+
333
+ def validate(self, l_max: float = L_MAX_DEFAULT) -> CapabilityValidationResult:
334
+ """Run A1 gate checks and return a structured result."""
335
+ cap = self.load()
336
+ if cap is None:
337
+ return CapabilityValidationResult(
338
+ valid=False,
339
+ textual_mode_allowed=False,
340
+ reasons=["No valid capability declaration found (Ωᵢ not set)."],
341
+ agent_id="<none>",
342
+ pst_status=PST_STATUS_NOT_RUN,
343
+ lipschitz_bound=float("inf"),
344
+ is_expired=True,
345
+ )
346
+ allowed, reasons = cap.textual_mode_allowed(l_max=l_max)
347
+ return CapabilityValidationResult(
348
+ valid=allowed,
349
+ textual_mode_allowed=allowed,
350
+ reasons=reasons,
351
+ agent_id=cap.agent_id,
352
+ pst_status=cap.pst_status,
353
+ lipschitz_bound=cap.lipschitz_bound,
354
+ is_expired=cap.is_expired(),
355
+ )
356
+
357
+ # ------------------------------------------------------------------
358
+ # Role / policy persistence
359
+ # ------------------------------------------------------------------
360
+
361
+ def save_role(self, role: AgentRole) -> None:
362
+ self.roles_dir.mkdir(parents=True, exist_ok=True)
363
+ path = self.roles_dir / f"{role.role_id}.json"
364
+ path.write_text(
365
+ json.dumps(role.to_dict(), indent=2, sort_keys=True) + "\n",
366
+ encoding="utf-8",
367
+ )
368
+
369
+ def load_role(self, role_id: str) -> Optional[AgentRole]:
370
+ path = self.roles_dir / f"{role_id}.json"
371
+ if not path.exists():
372
+ return None
373
+ try:
374
+ data = json.loads(path.read_text(encoding="utf-8"))
375
+ except (json.JSONDecodeError, OSError):
376
+ return None
377
+ try:
378
+ return AgentRole.from_dict(data)
379
+ except (TypeError, ValueError):
380
+ return None
381
+
382
+ def list_roles(self) -> List[str]:
383
+ if not self.roles_dir.exists():
384
+ return []
385
+ roles = []
386
+ for path in self.roles_dir.iterdir():
387
+ if path.is_file() and path.suffix == ".json":
388
+ roles.append(path.stem)
389
+ return sorted(roles)
390
+
391
+ def save_policy(self, policy: RolePolicy) -> None:
392
+ self.dir.mkdir(parents=True, exist_ok=True)
393
+ self.policy_path.write_text(
394
+ json.dumps(policy.to_dict(), indent=2, sort_keys=True) + "\n",
395
+ encoding="utf-8",
396
+ )
397
+
398
+ def load_policy(self) -> Optional[RolePolicy]:
399
+ if not self.policy_path.exists():
400
+ return None
401
+ try:
402
+ data = json.loads(self.policy_path.read_text(encoding="utf-8"))
403
+ except (json.JSONDecodeError, OSError):
404
+ return None
405
+ try:
406
+ return RolePolicy.from_dict(data)
407
+ except (TypeError, ValueError):
408
+ return None
409
+
410
+ def check_role_policy(
411
+ self,
412
+ cap: OmegaCapability,
413
+ role: AgentRole,
414
+ concepts_used: List[str],
415
+ ) -> List[str]:
416
+ """
417
+ Return a list of human-readable reasons if `cap` does not satisfy the
418
+ constraints of `role` for `concepts_used`. An empty list means allowed.
419
+ """
420
+ reasons: List[str] = []
421
+
422
+ # Required capability fields
423
+ for field in role.required_capabilities:
424
+ if not hasattr(cap, field):
425
+ reasons.append(f"Missing required capability field '{field}'.")
426
+ continue
427
+ value = getattr(cap, field)
428
+ if value is None:
429
+ reasons.append(f"Required capability field '{field}' is unset.")
430
+ elif isinstance(value, (int, float)) and value == 0:
431
+ # zero numeric fields are considered present
432
+ pass
433
+ elif isinstance(value, str) and not value.strip():
434
+ reasons.append(f"Required capability field '{field}' is empty.")
435
+
436
+ # Temperature ceiling
437
+ if role.max_temperature is not None and cap.temperature_used > role.max_temperature:
438
+ reasons.append(
439
+ f"temperature_used={cap.temperature_used} exceeds "
440
+ f"role max_temperature={role.max_temperature}."
441
+ )
442
+
443
+ # Disallowed concepts
444
+ for concept in concepts_used:
445
+ if concept in role.disallowed_concepts:
446
+ reasons.append(f"Role may not touch disallowed concept '{concept}'.")
447
+
448
+ # Allowed-concept whitelist
449
+ if role.allowed_concepts:
450
+ allowed = set(role.allowed_concepts)
451
+ for concept in concepts_used:
452
+ if concept not in allowed:
453
+ reasons.append(
454
+ f"Role is not allowed to touch concept '{concept}'."
455
+ )
456
+
457
+ return reasons
458
+
459
+
460
+ def apply_default_policy(store: CapabilityStore) -> RolePolicy:
461
+ """
462
+ Create a sensible default role policy and persist it through `store`.
463
+
464
+ Returns the policy object.
465
+ """
466
+ roles = [
467
+ AgentRole(
468
+ role_id="architect",
469
+ name="System Architect",
470
+ allowed_concepts=["design", "architecture", "security", "api"],
471
+ disallowed_concepts=["payments", "secrets", "pii"],
472
+ required_capabilities=["lipschitz_bound", "embedding_model"],
473
+ max_temperature=1.0,
474
+ ),
475
+ AgentRole(
476
+ role_id="implementer",
477
+ name="Implementer",
478
+ allowed_concepts=["code", "tests", "api", "schema"],
479
+ disallowed_concepts=["payments", "secrets", "pii"],
480
+ required_capabilities=["lipschitz_bound"],
481
+ max_temperature=1.2,
482
+ ),
483
+ AgentRole(
484
+ role_id="reviewer",
485
+ name="Reviewer",
486
+ allowed_concepts=["code", "tests", "security", "api", "schema"],
487
+ disallowed_concepts=["payments", "secrets", "pii"],
488
+ required_capabilities=["lipschitz_bound", "lipschitz_p95", "pst_status"],
489
+ max_temperature=0.5,
490
+ ),
491
+ AgentRole(
492
+ role_id="ops",
493
+ name="Operations",
494
+ allowed_concepts=["infrastructure", "deployment", "config"],
495
+ disallowed_concepts=["pii", "payments", "auth"],
496
+ required_capabilities=["lipschitz_bound"],
497
+ max_temperature=0.3,
498
+ ),
499
+ ]
500
+ policy = RolePolicy(roles=roles, default_role="implementer")
501
+ for role in roles:
502
+ store.save_role(role)
503
+ store.save_policy(policy)
504
+ return policy
505
+
506
+
507
+ # ---------------------------------------------------------------------------
508
+ # PST runner
509
+ # ---------------------------------------------------------------------------
510
+
511
+ @dataclasses.dataclass
512
+ class PSTReport:
513
+ """Structured output from a PSTRunner.run() call."""
514
+
515
+ agent_id: str
516
+ n_prompts: int
517
+ n_repetitions: int
518
+ pst_score: float
519
+ per_prompt_scores: List[float]
520
+ status: str # "passed" | "failed"
521
+ threshold: float
522
+ run_at: str
523
+
524
+
525
+ # ---------------------------------------------------------------------------
526
+ # A1 Round Cap (S9)
527
+ # ---------------------------------------------------------------------------
528
+
529
+ T_MAX_MULTIPLIER: float = 2.0 # T_max = T_MAX_MULTIPLIER × T_txt
530
+ CONVERGENCE_TIMEOUT_ROUNDS: int = 20 # Absolute fallback cap
531
+
532
+
533
+ @dataclasses.dataclass
534
+ class RoundCapResult:
535
+ allowed: bool # whether next round is allowed
536
+ current_round: int
537
+ t_max: int
538
+ reason: str # human-readable explanation
539
+
540
+
541
+ def check_round_cap(
542
+ current_round: int,
543
+ t_txt: int,
544
+ t_max_multiplier: float = T_MAX_MULTIPLIER,
545
+ absolute_cap: int = CONVERGENCE_TIMEOUT_ROUNDS,
546
+ ) -> RoundCapResult:
547
+ """
548
+ Check whether a Textual Mode round is permitted under A1 round cap.
549
+ T_max = floor(t_max_multiplier × t_txt), capped at absolute_cap.
550
+ Returns RoundCapResult with allowed=False and CONVERGENCE_TIMEOUT reason
551
+ if current_round > T_max.
552
+ """
553
+ t_max = min(int(t_max_multiplier * t_txt), absolute_cap)
554
+ if current_round > t_max:
555
+ return RoundCapResult(
556
+ allowed=False,
557
+ current_round=current_round,
558
+ t_max=t_max,
559
+ reason=f"CONVERGENCE_TIMEOUT: round {current_round} exceeds T_max={t_max} "
560
+ f"(= {t_max_multiplier}×T_txt={t_txt}, cap={absolute_cap})",
561
+ )
562
+ return RoundCapResult(
563
+ allowed=True,
564
+ current_round=current_round,
565
+ t_max=t_max,
566
+ reason="ok",
567
+ )
568
+
569
+
570
+ class PSTRunner:
571
+ """
572
+ Prompt Stability Test (PST) harness.
573
+
574
+ Runs `n_prompts` canonical prompts × `n_repetitions` repetitions through
575
+ a caller-supplied `run_fn(prompt: str, rep: int) -> str`. The per-prompt
576
+ stability score is the fraction of repetitions that produced the most
577
+ common output. The overall PST_score is the mean across all prompts.
578
+
579
+ Example (deterministic stub):
580
+ runner = PSTRunner(
581
+ agent_id="agent-1",
582
+ run_fn=lambda p, r: "always the same",
583
+ )
584
+ report = runner.run() # pst_score == 1.0, status == "passed"
585
+
586
+ The OSS-local mode intentionally does *not* call an external LLM; callers
587
+ supply their own run_fn, which can wrap any LLM client or a local stub.
588
+ """
589
+
590
+ DEFAULT_PROMPTS: List[str] = [
591
+ "Summarise the current reasoning context in one sentence.",
592
+ "What is the primary constraint in the current decision?",
593
+ "List the top three risks in the current plan.",
594
+ "Is the current approach deterministic? Answer yes or no.",
595
+ "What data is marked PRIVATE in the current context?",
596
+ ]
597
+
598
+ def __init__(
599
+ self,
600
+ agent_id: str,
601
+ run_fn: Callable[[str, int], str],
602
+ n_prompts: int = 5,
603
+ n_repetitions: int = 3,
604
+ threshold: float = PST_SCORE_THRESHOLD,
605
+ prompts: Optional[List[str]] = None,
606
+ ) -> None:
607
+ self.agent_id = agent_id
608
+ self.run_fn = run_fn
609
+ self.n_prompts = n_prompts
610
+ self.n_repetitions = n_repetitions
611
+ self.threshold = threshold
612
+ self.prompts = (prompts if prompts is not None else self.DEFAULT_PROMPTS)[: n_prompts]
613
+
614
+ def run(self) -> PSTReport:
615
+ """Execute the PST and return a PSTReport."""
616
+ per_prompt_scores: List[float] = []
617
+ for prompt in self.prompts:
618
+ outputs = [self.run_fn(prompt, rep) for rep in range(self.n_repetitions)]
619
+ most_common_count = Counter(outputs).most_common(1)[0][1]
620
+ stability = most_common_count / self.n_repetitions
621
+ per_prompt_scores.append(stability)
622
+
623
+ pst_score = (
624
+ sum(per_prompt_scores) / len(per_prompt_scores)
625
+ if per_prompt_scores
626
+ else 0.0
627
+ )
628
+ status = PST_STATUS_PASSED if pst_score >= self.threshold else PST_STATUS_FAILED
629
+
630
+ return PSTReport(
631
+ agent_id=self.agent_id,
632
+ n_prompts=self.n_prompts,
633
+ n_repetitions=self.n_repetitions,
634
+ pst_score=round(pst_score, 6),
635
+ per_prompt_scores=[round(s, 6) for s in per_prompt_scores],
636
+ status=status,
637
+ threshold=self.threshold,
638
+ run_at=_dt.datetime.now(tz=_dt.timezone.utc).isoformat(),
639
+ )
@@ -0,0 +1 @@
1
+ """Cloud MCP server client configuration and IDE wiring."""