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
devtorch_core/gcc.py ADDED
@@ -0,0 +1,1246 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import datetime as _dt
5
+ import hashlib
6
+ import json
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
9
+
10
+ from devtorch_core.storage import LocalFileBackend, StorageBackend
11
+ from devtorch_core.signing import sign_line_if_enabled, verify_log, signing_enabled
12
+
13
+ if TYPE_CHECKING:
14
+ from .capability import OmegaCapability
15
+ from .sensitivity import SensitivityEvent
16
+
17
+
18
+ GCC_DIR_NAME = ".GCC"
19
+ GCC_VERSION = "gcc-v0"
20
+ EVENT_LOG_NAME = "events.log.jsonl"
21
+ VERSION_FILE_NAME = "VERSION"
22
+ MAIN_MD_NAME = "main.md"
23
+ LOG_MD_NAME = "log.md"
24
+ CAPABILITIES_DIR_NAME = "capabilities"
25
+ OMEGA_FILE_NAME = "omega.json"
26
+
27
+ # Sprint 1 layout additions
28
+ REFS_DIR_NAME = "refs"
29
+ BRANCHES_DIR_NAME = "branches"
30
+ HEAD_FILE_NAME = "HEAD"
31
+ COMMITS_DIR_NAME = "commits"
32
+ COMMIT_INDEX_FILE_NAME = "COMMIT_INDEX"
33
+
34
+ # Sprint 2 layout additions
35
+ CONTEXT_DIR_NAME = "context_bundles"
36
+ NODE_STATE_FILE_NAME = "NODE_STATE"
37
+
38
+ # Sprint 3 layout additions
39
+ SENSITIVITIES_DIR_NAME = "sensitivities"
40
+ PST_REPORTS_DIR_NAME = "pst_reports"
41
+
42
+ # Sprint 4 layout additions
43
+ CONCEPTS_DIR_NAME = "concepts"
44
+ CONSENSUS_LOCK_FILE = "consensus_lock.json"
45
+
46
+ # Sprint 5 layout additions
47
+ REP_DIR_NAME = "rep"
48
+ SIS_DIR_NAME = "sis"
49
+ PROMPTS_DIR_NAME = "prompts"
50
+ DELTAF_DIR_NAME = "deltaf"
51
+ RDP_DIR_NAME = "rdp"
52
+
53
+
54
+ @dataclasses.dataclass
55
+ class GCCIssue:
56
+ kind: str
57
+ message: str
58
+
59
+
60
+ class GCCRepository:
61
+ """
62
+ Minimal Git-Context-Controller-like store for S0/S1.
63
+
64
+ Sprint 0 responsibilities:
65
+ - Create `.GCC/` layout and version marker.
66
+ - Maintain append-only JSONL event log with hash chain.
67
+ - Provide a happy-path `commit` that updates main.md and log.md.
68
+ - Provide `doctor` to validate layout + basic integrity.
69
+
70
+ Sprint 1 additions (branching and merges):
71
+ - Deterministic commit IDs (no wall-clock dependence).
72
+ - Branch metadata (`refs/HEAD`, `refs/branches/*`) and commit objects (`commits/*`).
73
+ - Basic history traversal and fast-forward merges.
74
+
75
+ Sprint 2 additions (context retrieval and node state):
76
+ - Simple node state machine persisted to `.GCC/NODE_STATE`.
77
+ - MECW-aware context bundles that select a subset of artifacts under a token budget K.
78
+ - Policy scaffolding for future PROTECTED/PRIVATE redaction (Sprint 5 will enforce it).
79
+
80
+ Sprint 3 additions (sensitivity schema, Θ store, Ωᵢ, PST runner):
81
+ - Structured SensitivityEvent schema with disclosure levels + storage/indexing.
82
+ - Θ coordination vector updated via pluggable Aggφ on each sensitivity add.
83
+ - OmegaCapability (v2.1 A1 fields) stored in `.GCC/capabilities/omega.json`.
84
+ - A1 Textual Mode gate: blocks mode if L̂ > L_max, expired calibration, or PST failed.
85
+ - PST runner harness (N_pst × N_rep) persists report artifacts in `.GCC/pst_reports/`.
86
+
87
+ Sprint 4 additions (invariants I1/I3, variance, consensus lock):
88
+ - Invariant engine: I1 (commit-backed decisions), I3 (concept handshake). Merge/lock-release gated.
89
+ - Concept store under `.GCC/concepts/` with hashing for semantic grounding.
90
+ - Variance calibration and f_max; VARIANCE_ALERT when f_max drops; reputation variance penalty.
91
+ - Consensus lock/unlock primitives; lock state in `.GCC/consensus_lock.json`.
92
+
93
+ Sprint 5 additions (REP, SIS A3, privacy A4):
94
+ - REP envelope + local transport ledger (record/replay, deterministic checksum).
95
+ - SIS-TC corpus + evaluation harness (FPR/FNR); quarantine decision tree; QUARANTINE events.
96
+ - RACP_SYSTEM_PROMPT_v2.1 artifact; Δf estimation + expiry; RDP accountant + PRIVACY_BUDGET_EXHAUSTED.
97
+ - Disclosure policy: [PRIVATE] suppression, padding, receiver mandate.
98
+ """
99
+
100
+ def __init__(self, root: Path, backend: StorageBackend | None = None) -> None:
101
+ self.root = root
102
+ self.gcc_dir = root / GCC_DIR_NAME
103
+ self._backend: StorageBackend = backend if backend is not None else LocalFileBackend(self.gcc_dir)
104
+
105
+ @classmethod
106
+ def at(cls, root: Path, backend: "StorageBackend | None" = None) -> "GCCRepository":
107
+ return cls(root, backend=backend)
108
+
109
+ def _read(self, *parts: str) -> str:
110
+ """Read a file key as UTF-8 text. Raises FileNotFoundError if missing."""
111
+ key = "/".join(parts)
112
+ return self._backend.read_bytes(key).decode("utf-8")
113
+
114
+ def _write(self, *parts: str, content: str) -> None:
115
+ """Write UTF-8 text to a file key."""
116
+ key = "/".join(parts)
117
+ self._backend.write_bytes(key, content.encode("utf-8"))
118
+
119
+ def is_initialized(self) -> bool:
120
+ return self._backend.exists(VERSION_FILE_NAME)
121
+
122
+ def init(self) -> None:
123
+ """Create the initial `.GCC/` layout and stub capability declaration file."""
124
+ self.gcc_dir.mkdir(parents=True, exist_ok=True)
125
+
126
+ # Version marker
127
+ self._write(VERSION_FILE_NAME, content=f"{GCC_VERSION}\n")
128
+
129
+ # Event log (append-only JSONL)
130
+ if not self._backend.exists(EVENT_LOG_NAME):
131
+ self._backend.write_bytes(EVENT_LOG_NAME, b"")
132
+
133
+ # Roadmap + reasoning log artifacts
134
+ if not self._backend.exists(MAIN_MD_NAME):
135
+ self._write(MAIN_MD_NAME, content="# DevTorch reasoning roadmap (main)\n\n")
136
+ if not self._backend.exists(LOG_MD_NAME):
137
+ self._write(LOG_MD_NAME, content="# DevTorch reasoning log\n\n")
138
+
139
+ # Capabilities stub (Ωᵢ)
140
+ caps_dir = self.gcc_dir / CAPABILITIES_DIR_NAME
141
+ caps_dir.mkdir(exist_ok=True)
142
+ omega_key = f"{CAPABILITIES_DIR_NAME}/{OMEGA_FILE_NAME}"
143
+ if not self._backend.exists(omega_key):
144
+ omega_stub = {
145
+ "version": 0,
146
+ "schema": "omega-stub-v0",
147
+ "note": "Capability declaration stub; to be expanded in later sprints.",
148
+ "created_at": _now_iso(),
149
+ }
150
+ self._write(omega_key, content=json.dumps(omega_stub, indent=2, sort_keys=True) + "\n")
151
+
152
+ # Sprint 1: refs + commits layout
153
+ refs_dir = self.gcc_dir / REFS_DIR_NAME
154
+ branches_dir = refs_dir / BRANCHES_DIR_NAME
155
+ commits_dir = self.gcc_dir / COMMITS_DIR_NAME
156
+ context_dir = self.gcc_dir / CONTEXT_DIR_NAME
157
+ refs_dir.mkdir(exist_ok=True)
158
+ branches_dir.mkdir(exist_ok=True)
159
+ commits_dir.mkdir(exist_ok=True)
160
+ context_dir.mkdir(exist_ok=True)
161
+
162
+ head_key = f"{REFS_DIR_NAME}/{HEAD_FILE_NAME}"
163
+ if not self._backend.exists(head_key):
164
+ # Default branch is always "main"
165
+ self._write(head_key, content="main\n")
166
+
167
+ main_ref_key = f"{REFS_DIR_NAME}/{BRANCHES_DIR_NAME}/main"
168
+ if not self._backend.exists(main_ref_key):
169
+ # Empty main branch (no commits yet)
170
+ self._write(main_ref_key, content="")
171
+
172
+ # Sprint 2: minimal node state
173
+ if not self._backend.exists(NODE_STATE_FILE_NAME):
174
+ self._write(
175
+ NODE_STATE_FILE_NAME,
176
+ content=json.dumps(
177
+ {
178
+ "current_branch": "main",
179
+ "mode": "idle", # idle | running | paused
180
+ "created_at": _now_iso(),
181
+ },
182
+ indent=2,
183
+ sort_keys=True,
184
+ )
185
+ + "\n",
186
+ )
187
+
188
+ # Sprint 3: sensitivities dir + PST reports dir + initial Θ
189
+ sens_dir = self.gcc_dir / SENSITIVITIES_DIR_NAME
190
+ pst_dir = self.gcc_dir / PST_REPORTS_DIR_NAME
191
+ sens_dir.mkdir(exist_ok=True)
192
+ pst_dir.mkdir(exist_ok=True)
193
+
194
+ from .theta import make_theta_store
195
+ theta_store = make_theta_store(self.gcc_dir)
196
+ if not theta_store.theta_path.exists():
197
+ theta_store.save({"version": 1, "updated_at": None, "coordination_vector": {}})
198
+
199
+ # Sprint 4: concepts dir + variance dir
200
+ (self.gcc_dir / CONCEPTS_DIR_NAME).mkdir(exist_ok=True)
201
+ from .variance import VARIANCE_DIR_NAME
202
+ (self.gcc_dir / VARIANCE_DIR_NAME).mkdir(exist_ok=True)
203
+
204
+ # Sprint 5: rep, sis, prompts, deltaf, rdp
205
+ (self.gcc_dir / REP_DIR_NAME).mkdir(exist_ok=True)
206
+ (self.gcc_dir / SIS_DIR_NAME).mkdir(exist_ok=True)
207
+ (self.gcc_dir / PROMPTS_DIR_NAME).mkdir(exist_ok=True)
208
+ (self.gcc_dir / DELTAF_DIR_NAME).mkdir(exist_ok=True)
209
+ (self.gcc_dir / RDP_DIR_NAME).mkdir(exist_ok=True)
210
+
211
+ # Sprint 5 — RACP: write default system prompt if not set
212
+ from .prompt_artifact import DEFAULT_PROMPT_CONTENT as _RACP_DEFAULT
213
+ if not self.prompt_get():
214
+ self.prompt_set(_RACP_DEFAULT)
215
+
216
+ # Record init event
217
+ self._append_event(event_type="INIT", payload={"gcc_version": GCC_VERSION})
218
+
219
+ def commit(self, message: str = "", agent_id: Optional[str] = None,
220
+ concepts: Optional[list[str]] = None) -> None:
221
+ """
222
+ Sprint-0 commit:
223
+ - Append a tiny entry to main.md and log.md.
224
+ - Record a COMMIT event into the JSONL log with hash chain.
225
+ """
226
+ if not self.is_initialized():
227
+ raise RuntimeError("GCC repository is not initialized. Run `devtorch init` first.")
228
+
229
+ # Sprint 1: commit metadata and deterministic ID
230
+ branch = self._current_branch()
231
+ parent_id = self._read_branch_tip(branch)
232
+ commit_id = self._next_commit_id()
233
+
234
+ ts = _now_iso()
235
+ _concepts = concepts or []
236
+ entry_header = f"## Commit at {ts}\n"
237
+ entry_body = (message or "(no message)") + "\n\n"
238
+ if _concepts:
239
+ entry_body = f"**concepts:** {', '.join(_concepts)}\n\n{entry_body}"
240
+
241
+ existing_main = self._read(MAIN_MD_NAME) if self._backend.exists(MAIN_MD_NAME) else ""
242
+ self._write(MAIN_MD_NAME, content=existing_main + entry_header + entry_body)
243
+
244
+ existing_log = self._read(LOG_MD_NAME) if self._backend.exists(LOG_MD_NAME) else ""
245
+ self._write(LOG_MD_NAME, content=existing_log + entry_header + entry_body)
246
+
247
+ # Persist commit object
248
+ commit_obj = {
249
+ "id": commit_id,
250
+ "branch": branch,
251
+ "parent": parent_id,
252
+ "message": message or "",
253
+ "timestamp": ts,
254
+ "concepts": _concepts,
255
+ "artifacts": {
256
+ "main": str(MAIN_MD_NAME),
257
+ "log": str(LOG_MD_NAME),
258
+ "capabilities": str(Path(CAPABILITIES_DIR_NAME) / OMEGA_FILE_NAME),
259
+ },
260
+ }
261
+ if agent_id is not None:
262
+ commit_obj["agent_id"] = agent_id
263
+ self._write(COMMITS_DIR_NAME, f"{commit_id}.json",
264
+ content=json.dumps(commit_obj, indent=2, sort_keys=True) + "\n")
265
+
266
+ # Update branch tip
267
+ self._write_branch_tip(branch, commit_id)
268
+
269
+ # Record COMMIT event including lineage
270
+ self._append_event(
271
+ event_type="COMMIT",
272
+ payload={
273
+ "message": message or "",
274
+ "timestamp": ts,
275
+ "branch": branch,
276
+ "commit_id": commit_id,
277
+ "parent": parent_id,
278
+ "concepts": _concepts,
279
+ },
280
+ )
281
+
282
+ # Sprint 1 public API
283
+
284
+ def create_branch(self, name: str, from_ref: Optional[str] = None) -> None:
285
+ """Create a new branch pointing at from_ref (or current tip if omitted)."""
286
+ if not name:
287
+ raise ValueError("Branch name must be non-empty.")
288
+ if "/" in name or name.strip() != name:
289
+ raise ValueError("Branch name must not contain '/' or surrounding whitespace.")
290
+
291
+ branches_dir = self.gcc_dir / REFS_DIR_NAME / BRANCHES_DIR_NAME
292
+ branches_dir.mkdir(parents=True, exist_ok=True)
293
+
294
+ if from_ref is None:
295
+ from_ref = self._read_branch_tip(self._current_branch())
296
+
297
+ (branches_dir / name).write_text(from_ref or "", encoding="utf-8")
298
+ self._append_event(event_type="BRANCH", payload={"name": name, "from": from_ref})
299
+
300
+ def history(self, branch: Optional[str] = None) -> List[dict]:
301
+ """
302
+ Return commit history for a branch, newest first.
303
+ """
304
+ if branch is None:
305
+ branch = self._current_branch()
306
+
307
+ tip = self._read_branch_tip(branch)
308
+ commits_dir = self.gcc_dir / COMMITS_DIR_NAME
309
+ history: List[dict] = []
310
+
311
+ while tip:
312
+ commit_path = commits_dir / f"{tip}.json"
313
+ if not commit_path.exists():
314
+ break
315
+ obj = json.loads(commit_path.read_text(encoding="utf-8"))
316
+ history.append(obj)
317
+ tip = obj.get("parent")
318
+
319
+ return history
320
+
321
+ def merge_fast_forward(self, source: str, target: Optional[str] = None) -> None:
322
+ """
323
+ Fast-forward merge: make target branch tip equal to source tip when target is an ancestor.
324
+ """
325
+ if target is None:
326
+ target = self._current_branch()
327
+
328
+ branches_dir = self.gcc_dir / REFS_DIR_NAME / BRANCHES_DIR_NAME
329
+ commits_dir = self.gcc_dir / COMMITS_DIR_NAME
330
+
331
+ source_tip = self._read_branch_tip(source)
332
+ target_tip = self._read_branch_tip(target)
333
+
334
+ if not source_tip:
335
+ raise RuntimeError(f"Source branch `{source}` has no commits to merge.")
336
+
337
+ # Sprint 4: I1 (commit-backed) invariant before any merge logic
338
+ from .invariants import (
339
+ InvariantContext,
340
+ InvariantEngine,
341
+ check_i1_commit_backed,
342
+ make_i3_semantic_handshake,
343
+ )
344
+ from .invariants import ConceptStore as InvariantConceptStore
345
+ context = InvariantContext(
346
+ operation="merge",
347
+ branch_tips={source: source_tip, target: target_tip or ""},
348
+ concepts_used=[],
349
+ )
350
+ engine = InvariantEngine()
351
+ engine.register("I1", check_i1_commit_backed)
352
+ concept_store = InvariantConceptStore(self.gcc_dir)
353
+ engine.register("I3", make_i3_semantic_handshake(concept_store))
354
+ failures = engine.evaluate(self, context)
355
+ if failures:
356
+ raise RuntimeError(
357
+ f"Invariant check failed: {failures[0].message}. Fix: {failures[0].actionable_fix}"
358
+ )
359
+
360
+ # Walk ancestors of source to see if target_tip is an ancestor (or empty for fast-forward from root).
361
+ ancestor = source_tip
362
+ target_is_ancestor = target_tip is None
363
+ while ancestor and not target_is_ancestor:
364
+ if ancestor == target_tip:
365
+ target_is_ancestor = True
366
+ break
367
+ commit_path = commits_dir / f"{ancestor}.json"
368
+ if not commit_path.exists():
369
+ break
370
+ obj = json.loads(commit_path.read_text(encoding="utf-8"))
371
+ ancestor = obj.get("parent")
372
+
373
+ if not target_is_ancestor:
374
+ raise RuntimeError("Non fast-forward merges are not yet supported in Sprint 1.")
375
+
376
+ # Update target branch tip
377
+ (branches_dir / target).write_text(source_tip, encoding="utf-8")
378
+
379
+ # Append a small note into main/log to preserve lineage information.
380
+ main_md = self.gcc_dir / MAIN_MD_NAME
381
+ log_md = self.gcc_dir / LOG_MD_NAME
382
+ ts = _now_iso()
383
+ note = f"### Merge fast-forward `{target}` <- `{source}` at {ts} (tip {source_tip})\n\n"
384
+ for path in (main_md, log_md):
385
+ with path.open("a", encoding="utf-8") as f:
386
+ f.write(note)
387
+
388
+ self._append_event(
389
+ event_type="MERGE",
390
+ payload={
391
+ "source": source,
392
+ "target": target,
393
+ "new_tip": source_tip,
394
+ "old_target_tip": target_tip,
395
+ },
396
+ )
397
+
398
+ # Sprint 4 public API: consensus lock, concepts, variance
399
+
400
+ def _lock_path(self) -> Path:
401
+ return self.gcc_dir / CONSENSUS_LOCK_FILE
402
+
403
+ def is_locked(self) -> bool:
404
+ p = self._lock_path()
405
+ if not p.exists():
406
+ return False
407
+ try:
408
+ data = json.loads(p.read_text(encoding="utf-8"))
409
+ return bool(data.get("locked", False))
410
+ except (json.JSONDecodeError, OSError):
411
+ return False
412
+
413
+ def lock(self, reason: str = "", required_peer_count: Optional[int] = None) -> None:
414
+ """Acquire consensus lock. Optional required_peer_count can be checked against f_max."""
415
+ if not self.is_initialized():
416
+ raise RuntimeError("GCC repository is not initialized. Run `devtorch init` first.")
417
+ if self.is_locked():
418
+ raise RuntimeError("Repository is already locked. Run `devtorch unlock` first.")
419
+ payload: dict = {
420
+ "locked": True,
421
+ "reason": reason or "consensus lock",
422
+ "locked_at": _now_iso(),
423
+ }
424
+ if required_peer_count is not None:
425
+ payload["required_peer_count"] = required_peer_count
426
+ self._lock_path().write_text(
427
+ json.dumps(payload, indent=2) + "\n",
428
+ encoding="utf-8",
429
+ )
430
+ self._append_event(event_type="LOCK", payload=payload)
431
+
432
+ def unlock(self) -> None:
433
+ """Release consensus lock. I1 is run so that only commit-backed state can finalize."""
434
+ if not self.is_initialized():
435
+ raise RuntimeError("GCC repository is not initialized. Run `devtorch init` first.")
436
+ if not self.is_locked():
437
+ return
438
+ from .invariants import (
439
+ InvariantContext,
440
+ InvariantEngine,
441
+ check_i1_commit_backed,
442
+ )
443
+ branch = self._current_branch()
444
+ tip = self._read_branch_tip(branch)
445
+ context = InvariantContext(
446
+ operation="lock_release",
447
+ branch_tips={branch: tip or ""},
448
+ concepts_used=[],
449
+ )
450
+ engine = InvariantEngine()
451
+ engine.register("I1", check_i1_commit_backed)
452
+ failures = engine.evaluate(self, context)
453
+ if failures:
454
+ raise RuntimeError(
455
+ f"Cannot unlock: invariant I1 failed. {failures[0].message}. Fix: {failures[0].actionable_fix}"
456
+ )
457
+ payload = {"unlocked_at": _now_iso()}
458
+ self._lock_path().write_text(
459
+ json.dumps({"locked": False, **payload}, indent=2) + "\n",
460
+ encoding="utf-8",
461
+ )
462
+ self._append_event(event_type="UNLOCK", payload=payload)
463
+
464
+ def concept_add(self, name: str, definition: str) -> None:
465
+ """Add a concept definition for I3 semantic grounding."""
466
+ if not self.is_initialized():
467
+ raise RuntimeError("GCC repository is not initialized. Run `devtorch init` first.")
468
+ from .invariants import ConceptStore as InvariantConceptStore
469
+ store = InvariantConceptStore(self.gcc_dir)
470
+ store.add(name, definition)
471
+
472
+ def concept_list(self) -> List[str]:
473
+ """List registered concept names."""
474
+ if not self.is_initialized():
475
+ raise RuntimeError("GCC repository is not initialized. Run `devtorch init` first.")
476
+ from .invariants import ConceptStore as InvariantConceptStore
477
+ store = InvariantConceptStore(self.gcc_dir)
478
+ return store.list_concepts()
479
+
480
+ def variance_calibrate(self, agent_variances: Dict[str, float]) -> "VarianceReport":
481
+ """Run variance calibration and persist report under .GCC/variance/."""
482
+ if not self.is_initialized():
483
+ raise RuntimeError("GCC repository is not initialized. Run `devtorch init` first.")
484
+ from .variance import variance_calibrate as calibrate, VarianceStore, VarianceReport
485
+ report = calibrate(agent_variances)
486
+ VarianceStore(self.gcc_dir).save_report(report)
487
+ return report
488
+
489
+ def get_variance_report(self) -> Optional["VarianceReport"]:
490
+ """Load last variance calibration report if present."""
491
+ if not self.is_initialized():
492
+ return None
493
+ from .variance import VarianceStore, VarianceReport
494
+ return VarianceStore(self.gcc_dir).load_report()
495
+
496
+ def variance_monitor_run(
497
+ self,
498
+ agent_variances: Optional[Dict[str, float]] = None,
499
+ ) -> dict:
500
+ """
501
+ Run A2 rolling variance monitoring: compute/live-update f_max, persist live state.
502
+ Emits VARIANCE_ALERT when f_max drops or f_max_live=0; when f_max_live=0
503
+ deterministic mode is forced (see variance_deterministic_mode_forced).
504
+ Returns dict with f_max_live, previous_f_max, sigma_sq_obs, alert_emitted, deterministic_mode_forced.
505
+ """
506
+ if not self.is_initialized():
507
+ raise RuntimeError("GCC repository is not initialized. Run `devtorch init` first.")
508
+ from .variance import run_variance_monitor, VARIANCE_ALERT_EVENT
509
+ result = run_variance_monitor(self.gcc_dir, agent_variances)
510
+ if result.alert_emitted:
511
+ self._append_event(
512
+ event_type=VARIANCE_ALERT_EVENT,
513
+ payload={
514
+ "f_max_live": result.f_max_live,
515
+ "previous_f_max": result.previous_f_max,
516
+ "sigma_sq_obs": result.sigma_sq_obs,
517
+ "deterministic_mode_forced": result.deterministic_mode_forced,
518
+ },
519
+ )
520
+ return result.to_dict() if hasattr(result, "to_dict") else {
521
+ "f_max_live": result.f_max_live,
522
+ "previous_f_max": result.previous_f_max,
523
+ "sigma_sq_obs": result.sigma_sq_obs,
524
+ "alert_emitted": result.alert_emitted,
525
+ "deterministic_mode_forced": result.deterministic_mode_forced,
526
+ }
527
+
528
+ def variance_deterministic_mode_forced(self) -> bool:
529
+ """True when f_max_live was 0 at last variance monitor run (A2: force deterministic mode)."""
530
+ if not self.is_initialized():
531
+ return False
532
+ from .variance import variance_deterministic_mode_forced as check
533
+ return check(self.gcc_dir)
534
+
535
+ def invariant_check(
536
+ self,
537
+ operation: str = "merge",
538
+ branch_tips: Optional[Dict[str, str]] = None,
539
+ concepts_used: Optional[List[str]] = None,
540
+ ) -> List["InvariantFailure"]:
541
+ """Run invariant engine and return list of failures (empty if all pass)."""
542
+ if not self.is_initialized():
543
+ raise RuntimeError("GCC repository is not initialized. Run `devtorch init` first.")
544
+ from .invariants import (
545
+ InvariantContext,
546
+ InvariantEngine,
547
+ check_i1_commit_backed,
548
+ make_i3_semantic_handshake,
549
+ ConceptStore as InvariantConceptStore,
550
+ )
551
+ from .invariants import InvariantFailure
552
+ if branch_tips is None:
553
+ branch = self._current_branch()
554
+ tip = self._read_branch_tip(branch)
555
+ branch_tips = {branch: tip or ""}
556
+ context = InvariantContext(
557
+ operation=operation,
558
+ branch_tips=branch_tips,
559
+ concepts_used=concepts_used or [],
560
+ )
561
+ engine = InvariantEngine()
562
+ engine.register("I1", check_i1_commit_backed)
563
+ concept_store = InvariantConceptStore(self.gcc_dir)
564
+ engine.register("I3", make_i3_semantic_handshake(concept_store))
565
+ return engine.evaluate(self, context)
566
+
567
+ # Sprint 5 public API: REP, SIS, prompt, deltaf, RDP, disclosure
568
+
569
+ def rep_append(self, agent_id: str, round_id: int, payload: dict, trust: float = 0.5) -> int:
570
+ """Append a REP envelope to the local ledger; return sequence id."""
571
+ if not self.is_initialized():
572
+ raise RuntimeError("GCC repository is not initialized. Run `devtorch init` first.")
573
+ from .rep import REPLedger, create_envelope
574
+ envelope = create_envelope(agent_id, round_id, payload, trust)
575
+ ledger = REPLedger(self.gcc_dir)
576
+ return ledger.append(envelope, _now_iso())
577
+
578
+ def rep_replay_checksum(self) -> str:
579
+ """Deterministic checksum of REP ledger for replay verification."""
580
+ if not self.is_initialized():
581
+ return ""
582
+ from .rep import REPLedger
583
+ return REPLedger(self.gcc_dir).replay_checksum()
584
+
585
+ def sis_tc_run(self, corpus_path: Optional[Path] = None) -> "SISTCReport":
586
+ """Run SIS-TC evaluation; persist report under .GCC/sis/. corpus_path defaults to .GCC/sis/sis_tc_corpus.jsonl."""
587
+ if not self.is_initialized():
588
+ raise RuntimeError("GCC repository is not initialized. Run `devtorch init` first.")
589
+ from .sis import load_sis_tc_corpus, run_sis_tc_eval, SISTCReport, SIS_DIR_NAME, SIS_TC_CORPUS_NAME, SIS_TC_REPORT_NAME
590
+ path = corpus_path or (self.gcc_dir / SIS_DIR_NAME / SIS_TC_CORPUS_NAME)
591
+ corpus = load_sis_tc_corpus(path)
592
+ report = run_sis_tc_eval(corpus, classifier_fn=None, threshold=0.5)
593
+ (self.gcc_dir / SIS_DIR_NAME).mkdir(exist_ok=True)
594
+ (self.gcc_dir / SIS_DIR_NAME / SIS_TC_REPORT_NAME).write_text(
595
+ json.dumps(report.to_dict(), indent=2) + "\n", encoding="utf-8",
596
+ )
597
+ return report
598
+
599
+ def prompt_set(self, content: str, version: str = "v2.1") -> None:
600
+ """Store RACP_SYSTEM_PROMPT_v2.1 artifact."""
601
+ if not self.is_initialized():
602
+ raise RuntimeError("GCC repository is not initialized. Run `devtorch init` first.")
603
+ from .prompt_artifact import PromptArtifactStore
604
+ PromptArtifactStore(self.gcc_dir).set_prompt(content, version)
605
+
606
+ def prompt_get(self) -> Optional[str]:
607
+ """Return RACP system prompt content or None."""
608
+ if not self.is_initialized():
609
+ return None
610
+ from .prompt_artifact import PromptArtifactStore
611
+ return PromptArtifactStore(self.gcc_dir).get_prompt()
612
+
613
+ def deltaf_run(self, expiry_days: int = 90) -> "DeltaFReport":
614
+ """Run Δf estimation from current sensitivities; persist report and return it."""
615
+ if not self.is_initialized():
616
+ raise RuntimeError("GCC repository is not initialized. Run `devtorch init` first.")
617
+ from .deltaf import run_deltaf_estimation, DeltaFStore, DeltaFReport
618
+ events = self.list_sensitivities()
619
+ report = run_deltaf_estimation(events)
620
+ DeltaFStore(self.gcc_dir).save_report(report, _now_iso(), expiry_days)
621
+ return report
622
+
623
+ def rdp_spend(self, round_id: int, epsilon_cost: float) -> Tuple[bool, str]:
624
+ """Spend epsilon for round; returns (allowed: bool, event: str). Emits PRIVACY_BUDGET_EXHAUSTED if exhausted."""
625
+ if not self.is_initialized():
626
+ raise RuntimeError("GCC repository is not initialized. Run `devtorch init` first.")
627
+ from .rdp import RDPAccountant, PRIVACY_BUDGET_EXHAUSTED_EVENT
628
+ allowed, event = RDPAccountant(self.gcc_dir).spend(round_id, epsilon_cost)
629
+ if not allowed and event:
630
+ self._append_event(event_type=event, payload={"round_id": round_id})
631
+ return allowed, event
632
+
633
+ def rdp_read_only(self) -> bool:
634
+ """True if RDP budget exhausted (read-only mode)."""
635
+ if not self.is_initialized():
636
+ return False
637
+ from .rdp import RDPAccountant
638
+ return RDPAccountant(self.gcc_dir).is_read_only()
639
+
640
+ def disclosure_redact(self, text: str, padding_length: Optional[int] = None) -> str:
641
+ """Apply [PRIVATE] suppression and fixed-length padding."""
642
+ from .disclosure import DisclosurePolicy, apply_disclosure_policy
643
+ policy = DisclosurePolicy(padding_length=padding_length or 32)
644
+ return apply_disclosure_policy(text, policy)
645
+
646
+ # Sprint 6 debug views
647
+
648
+ def debug_timeline(self, filter_types: Optional[List[str]] = None) -> List[dict]:
649
+ """Read event log and return events, optionally filtered by event_type."""
650
+ if not self.is_initialized():
651
+ return []
652
+ event_log = self.gcc_dir / EVENT_LOG_NAME
653
+ if not event_log.exists():
654
+ return []
655
+ events = []
656
+ with event_log.open("r", encoding="utf-8") as f:
657
+ for line in f:
658
+ line = line.strip()
659
+ if not line:
660
+ continue
661
+ try:
662
+ obj = json.loads(line)
663
+ if filter_types is None or obj.get("event_type") in filter_types:
664
+ events.append(obj)
665
+ except json.JSONDecodeError:
666
+ continue
667
+ return events
668
+
669
+ def debug_branch_compare(self, branch1: str, branch2: str) -> dict:
670
+ """Compare two branches: history (commit ids, messages), tip artifacts. Θ is global so no per-branch diff."""
671
+ if not self.is_initialized():
672
+ return {"branch1": [], "branch2": [], "theta": {}}
673
+ h1 = self.history(branch1)
674
+ h2 = self.history(branch2)
675
+ theta = self.get_theta()
676
+ return {
677
+ "branch1": [{"id": c.get("id"), "message": c.get("message"), "timestamp": c.get("timestamp")} for c in h1],
678
+ "branch2": [{"id": c.get("id"), "message": c.get("message"), "timestamp": c.get("timestamp")} for c in h2],
679
+ "theta": theta.get("coordination_vector", {}),
680
+ }
681
+
682
+ def debug_invariant_trace(self) -> List[dict]:
683
+ """Run invariant check and return failures as trace (invariant_id, message, actionable_fix)."""
684
+ if not self.is_initialized():
685
+ return []
686
+ failures = self.invariant_check()
687
+ return [
688
+ {"invariant_id": f.invariant_id, "message": f.message, "actionable_fix": f.actionable_fix}
689
+ for f in failures
690
+ ]
691
+
692
+ # Sprint 2 public API
693
+
694
+ def context_bundle(
695
+ self,
696
+ k_tokens: int,
697
+ policy: Optional[dict] = None,
698
+ branch: Optional[str] = None,
699
+ explain: bool = False,
700
+ ) -> dict:
701
+ """
702
+ Build a MECW-aware context bundle under an approximate token budget.
703
+
704
+ For Sprint 2 we keep the heuristic simple:
705
+ - Work on the given branch (or current HEAD branch).
706
+ - Traverse commits newest-first and include commit messages until
707
+ the approximate token budget is hit.
708
+ - Always include the current `main.md` header as a pinned artifact.
709
+
710
+ The returned bundle shape:
711
+ {
712
+ "k_tokens": int,
713
+ "approx_tokens_used": int,
714
+ "branch": str,
715
+ "included_commits": [commit_id, ...],
716
+ "artifacts": [
717
+ {"path": "main.md", "reason": "..."},
718
+ {"path": "commits/00000001.json", "reason": "..."},
719
+ ...
720
+ ],
721
+ "policy": policy or {},
722
+ }
723
+
724
+ The bundle is persisted under `.GCC/context_bundles/<timestamp>.json`.
725
+ """
726
+ if not self.is_initialized():
727
+ raise RuntimeError("GCC repository is not initialized. Run `devtorch init` first.")
728
+
729
+ if branch is None:
730
+ branch = self._current_branch()
731
+
732
+ history = self.history(branch)
733
+ approx_tokens_used = 0
734
+ included_commits: List[dict] = []
735
+
736
+ def _approx_tokens(text: str) -> int:
737
+ # Simple heuristic: 1 token ≈ 4 characters (rough).
738
+ return max(1, len(text) // 4)
739
+
740
+ # Always pin main.md header as context.
741
+ artifacts: List[dict] = []
742
+ main_md_path = self.gcc_dir / MAIN_MD_NAME
743
+ if main_md_path.exists():
744
+ header_preview = ""
745
+ with main_md_path.open("r", encoding="utf-8") as f:
746
+ for _ in range(5):
747
+ line = f.readline()
748
+ if not line:
749
+ break
750
+ header_preview += line
751
+ tokens = _approx_tokens(header_preview)
752
+ approx_tokens_used += tokens
753
+ artifacts.append(
754
+ {
755
+ "path": str(MAIN_MD_NAME),
756
+ "reason": "Pinned roadmap header for global migration context.",
757
+ "approx_tokens": tokens,
758
+ }
759
+ )
760
+
761
+ # Walk commit history newest-first and include messages until budget is reached.
762
+ commits_dir = self.gcc_dir / COMMITS_DIR_NAME
763
+ for commit in history:
764
+ cid = commit.get("id")
765
+ message = commit.get("message", "")
766
+ # Use the commit message itself as the reason so the context prefix
767
+ # carries the actual reasoning text, not just a generic label.
768
+ reason = message.strip() or f"Reasoning milestone on branch `{branch}`."
769
+ commit_tokens = _approx_tokens(reason)
770
+ if approx_tokens_used + commit_tokens > max(k_tokens, 1):
771
+ break
772
+ approx_tokens_used += commit_tokens
773
+ included_commits.append(commit)
774
+ artifacts.append(
775
+ {
776
+ "path": str(COMMITS_DIR_NAME + f"/{cid}.json"),
777
+ "reason": reason,
778
+ "approx_tokens": commit_tokens,
779
+ }
780
+ )
781
+
782
+ bundle = {
783
+ "k_tokens": int(k_tokens),
784
+ "approx_tokens_used": approx_tokens_used,
785
+ "branch": branch,
786
+ "included_commits": [c.get("id") for c in included_commits],
787
+ "artifacts": artifacts,
788
+ "policy": policy or {},
789
+ }
790
+
791
+ # Sprint 2: policy-aware retrieval scaffold.
792
+ # We don't yet enforce PROTECTED/PRIVATE redaction, but we attach
793
+ # the requested policy to the bundle for later sprints.
794
+
795
+ # Persist bundle
796
+ context_dir = self.gcc_dir / CONTEXT_DIR_NAME
797
+ context_dir.mkdir(exist_ok=True)
798
+ timestamp = _now_iso().replace(":", "-")
799
+ bundle_path = context_dir / f"bundle_{branch}_{timestamp}.json"
800
+ bundle_path.write_text(json.dumps(bundle, indent=2, sort_keys=True) + "\n", encoding="utf-8")
801
+
802
+ if explain:
803
+ bundle["bundle_path"] = str(bundle_path.relative_to(self.gcc_dir))
804
+
805
+ return bundle
806
+
807
+ def doctor(self) -> List[GCCIssue]:
808
+ """
809
+ Validate S0 invariants:
810
+ - `.GCC/` exists and contains core files.
811
+ - VERSION matches expected GCC_VERSION.
812
+ - Event log is valid JSONL and hash chain is intact.
813
+ """
814
+ issues: List[GCCIssue] = []
815
+
816
+ if not self.gcc_dir.exists():
817
+ issues.append(GCCIssue("layout", f"Missing `{GCC_DIR_NAME}/` directory. Run `devtorch init`."))
818
+ return issues
819
+
820
+ version_path = self.gcc_dir / VERSION_FILE_NAME
821
+ if not version_path.exists():
822
+ issues.append(GCCIssue("layout", f"Missing `{VERSION_FILE_NAME}` in `{GCC_DIR_NAME}/`."))
823
+ else:
824
+ version = version_path.read_text(encoding="utf-8").strip()
825
+ if version != GCC_VERSION:
826
+ issues.append(
827
+ GCCIssue(
828
+ "version",
829
+ f"Unexpected GCC version `{version}` (expected `{GCC_VERSION}`).",
830
+ )
831
+ )
832
+
833
+ # Check core artifacts
834
+ for name in (MAIN_MD_NAME, LOG_MD_NAME):
835
+ p = self.gcc_dir / name
836
+ if not p.exists():
837
+ issues.append(GCCIssue("layout", f"Missing `{name}` in `{GCC_DIR_NAME}/`."))
838
+
839
+ # Validate event log & hash chain
840
+ event_log = self.gcc_dir / EVENT_LOG_NAME
841
+ if not event_log.exists():
842
+ issues.append(GCCIssue("layout", f"Missing `{EVENT_LOG_NAME}` in `{GCC_DIR_NAME}/`."))
843
+ else:
844
+ chain_issues = _validate_event_log(event_log)
845
+ issues.extend(chain_issues)
846
+ sig_failures = verify_log(self.gcc_dir)
847
+ for failure in sig_failures:
848
+ line_no = failure["line_no"]
849
+ reason = failure["reason"]
850
+ event_hash = failure.get("event_hash")
851
+ msg = f"Signature failure on line {line_no}: {reason}"
852
+ if event_hash:
853
+ msg += f" (event_hash={event_hash})"
854
+ issues.append(GCCIssue("signature", msg))
855
+
856
+ return issues
857
+
858
+ # Sprint 3 public API
859
+
860
+ def add_sensitivity(self, event: "SensitivityEvent") -> str: # type: ignore[name-defined]
861
+ """
862
+ Persist *event*, ripple it into Θ, and emit a SENSITIVITY_ADDED audit event.
863
+ Returns the assigned event_id.
864
+ """
865
+ if not self.is_initialized():
866
+ raise RuntimeError("GCC repository is not initialized. Run `devtorch init` first.")
867
+
868
+ from .sensitivity import SensitivityStore
869
+ from .theta import make_theta_store
870
+
871
+ store = SensitivityStore(self.gcc_dir / SENSITIVITIES_DIR_NAME)
872
+ event_id = store.add(event)
873
+
874
+ make_theta_store(self.gcc_dir).ripple([event.to_dict()])
875
+
876
+ self._append_event(
877
+ event_type="SENSITIVITY_ADDED",
878
+ payload={
879
+ "event_id": event_id,
880
+ "target_concept": event.target_concept,
881
+ "disclosure_level": event.disclosure_level,
882
+ "source_node": event.source_node,
883
+ },
884
+ )
885
+ return event_id
886
+
887
+ def list_sensitivities(
888
+ self,
889
+ concept: Optional[str] = None,
890
+ disclosure_level: Optional[str] = None,
891
+ source_node: Optional[str] = None,
892
+ ) -> List[dict]:
893
+ """
894
+ Return stored sensitivity events as dicts, optionally filtered.
895
+
896
+ Parameters match SensitivityStore.filter(); all are wildcards when None.
897
+ """
898
+ if not self.is_initialized():
899
+ raise RuntimeError("GCC repository is not initialized. Run `devtorch init` first.")
900
+
901
+ from .sensitivity import SensitivityStore
902
+
903
+ store = SensitivityStore(self.gcc_dir / SENSITIVITIES_DIR_NAME)
904
+ return [
905
+ e.to_dict()
906
+ for e in store.filter(
907
+ concept=concept,
908
+ disclosure_level=disclosure_level,
909
+ source_node=source_node,
910
+ )
911
+ ]
912
+
913
+ def get_theta(self) -> dict:
914
+ """Return the current coordination vector Θ as a dict."""
915
+ if not self.is_initialized():
916
+ raise RuntimeError("GCC repository is not initialized. Run `devtorch init` first.")
917
+
918
+ from .theta import make_theta_store
919
+
920
+ return make_theta_store(self.gcc_dir).load()
921
+
922
+ def set_capability(self, cap: "OmegaCapability") -> None: # type: ignore[name-defined]
923
+ """
924
+ Persist a new Ωᵢ capability declaration and emit a CAPABILITY_SET audit event.
925
+ """
926
+ if not self.is_initialized():
927
+ raise RuntimeError("GCC repository is not initialized. Run `devtorch init` first.")
928
+
929
+ from .capability import CapabilityStore
930
+
931
+ CapabilityStore(self.gcc_dir / CAPABILITIES_DIR_NAME).save(cap)
932
+ self._append_event(
933
+ event_type="CAPABILITY_SET",
934
+ payload={
935
+ "agent_id": cap.agent_id,
936
+ "schema_version": cap.schema_version,
937
+ "lipschitz_bound": cap.lipschitz_bound,
938
+ "pst_status": cap.pst_status,
939
+ },
940
+ )
941
+
942
+ def get_capability(self) -> Optional[dict]:
943
+ """Return the stored Ωᵢ as a dict, or None if none has been set."""
944
+ if not self.is_initialized():
945
+ raise RuntimeError("GCC repository is not initialized. Run `devtorch init` first.")
946
+
947
+ from .capability import CapabilityStore
948
+
949
+ cap = CapabilityStore(self.gcc_dir / CAPABILITIES_DIR_NAME).load()
950
+ return cap.to_dict() if cap is not None else None
951
+
952
+ def validate_capability(self, l_max: Optional[float] = None) -> dict:
953
+ """
954
+ Run A1 gate checks.
955
+
956
+ If Textual Mode is not allowed, emits a CAPABILITY_DEGRADED event.
957
+ Returns a dict representation of CapabilityValidationResult.
958
+ """
959
+ if not self.is_initialized():
960
+ raise RuntimeError("GCC repository is not initialized. Run `devtorch init` first.")
961
+
962
+ from .capability import CapabilityStore, L_MAX_DEFAULT
963
+
964
+ effective_l_max = l_max if l_max is not None else L_MAX_DEFAULT
965
+ result = CapabilityStore(self.gcc_dir / CAPABILITIES_DIR_NAME).validate(l_max=effective_l_max)
966
+
967
+ # A2: variance forces deterministic mode when f_max_live=0
968
+ if self.variance_deterministic_mode_forced():
969
+ import dataclasses as _dc
970
+ result = _dc.replace(
971
+ result,
972
+ textual_mode_allowed=False,
973
+ reasons=[*result.reasons, "A2: f_max=0; deterministic mode forced (variance)."],
974
+ )
975
+
976
+ if not result.textual_mode_allowed:
977
+ self._append_event(
978
+ event_type="CAPABILITY_DEGRADED",
979
+ payload={
980
+ "agent_id": result.agent_id,
981
+ "reasons": result.reasons,
982
+ "pst_status": result.pst_status,
983
+ "lipschitz_bound": (
984
+ result.lipschitz_bound
985
+ if result.lipschitz_bound != float("inf")
986
+ else None
987
+ ),
988
+ },
989
+ )
990
+
991
+ import dataclasses
992
+
993
+ return dataclasses.asdict(result)
994
+
995
+ def run_pst(
996
+ self,
997
+ agent_id: str,
998
+ run_fn,
999
+ n_prompts: int = 5,
1000
+ n_repetitions: int = 3,
1001
+ threshold: Optional[float] = None,
1002
+ prompts: Optional[List[str]] = None,
1003
+ ) -> dict:
1004
+ """
1005
+ Execute a PST run, persist the report artifact, update Ωᵢ PST fields,
1006
+ and emit a PST_RUN audit event. Returns the report as a dict.
1007
+ """
1008
+ if not self.is_initialized():
1009
+ raise RuntimeError("GCC repository is not initialized. Run `devtorch init` first.")
1010
+
1011
+ import dataclasses as _dc
1012
+
1013
+ from .capability import (
1014
+ CapabilityStore,
1015
+ OmegaCapability,
1016
+ PSTRunner,
1017
+ PST_SCORE_THRESHOLD,
1018
+ )
1019
+
1020
+ effective_threshold = threshold if threshold is not None else PST_SCORE_THRESHOLD
1021
+ runner = PSTRunner(
1022
+ agent_id=agent_id,
1023
+ run_fn=run_fn,
1024
+ n_prompts=n_prompts,
1025
+ n_repetitions=n_repetitions,
1026
+ threshold=effective_threshold,
1027
+ prompts=prompts,
1028
+ )
1029
+ report = runner.run()
1030
+
1031
+ # Persist report artifact
1032
+ pst_dir = self.gcc_dir / PST_REPORTS_DIR_NAME
1033
+ pst_dir.mkdir(exist_ok=True)
1034
+ ts = _now_iso().replace(":", "-")
1035
+ report_path = pst_dir / f"pst_{agent_id}_{ts}.json"
1036
+ report_path.write_text(
1037
+ json.dumps(_dc.asdict(report), indent=2, sort_keys=True) + "\n",
1038
+ encoding="utf-8",
1039
+ )
1040
+
1041
+ # Update Ωᵢ if a capability declaration exists
1042
+ cap_store = CapabilityStore(self.gcc_dir / CAPABILITIES_DIR_NAME)
1043
+ cap = cap_store.load()
1044
+ if cap is not None:
1045
+ updated = _dc.replace(
1046
+ cap,
1047
+ pst_status=report.status,
1048
+ pst_score=report.pst_score,
1049
+ pst_n_prompts=report.n_prompts,
1050
+ pst_n_repetitions=report.n_repetitions,
1051
+ updated_at=_now_iso(),
1052
+ )
1053
+ cap_store.save(updated)
1054
+
1055
+ self._append_event(
1056
+ event_type="PST_RUN",
1057
+ payload={
1058
+ "agent_id": agent_id,
1059
+ "pst_score": report.pst_score,
1060
+ "status": report.status,
1061
+ "n_prompts": report.n_prompts,
1062
+ "n_repetitions": report.n_repetitions,
1063
+ },
1064
+ )
1065
+ return _dc.asdict(report)
1066
+
1067
+ def check_divergence(self, concept: str) -> List[dict]:
1068
+ """
1069
+ Detect reasoning divergence between agents for *concept*.
1070
+ Returns list of DivergenceSignal dicts (serialisable).
1071
+ """
1072
+ if not self.is_initialized():
1073
+ raise RuntimeError("GCC repository is not initialized.")
1074
+ import dataclasses
1075
+ from .divergence import DivergenceDetector
1076
+ signals = DivergenceDetector(self).detect(concept=concept)
1077
+ return [dataclasses.asdict(s) for s in signals]
1078
+
1079
+ def start_consolidation(self, concept: str) -> Optional[dict]:
1080
+ """
1081
+ Detect divergence and start a ConsolidationWorkflow for *concept*.
1082
+ Returns the ConsolidationRecord as a dict, or None if no divergence.
1083
+ """
1084
+ if not self.is_initialized():
1085
+ raise RuntimeError("GCC repository is not initialized.")
1086
+ import dataclasses
1087
+ from .divergence import DivergenceDetector
1088
+ from .consolidation import ConsolidationWorkflow
1089
+ signals = DivergenceDetector(self).detect(concept=concept)
1090
+ if not signals:
1091
+ return None
1092
+ record = ConsolidationWorkflow(self).start(concept=concept, signals=signals)
1093
+ return record.to_dict()
1094
+
1095
+ def reasoning_query(
1096
+ self,
1097
+ concept: str,
1098
+ agent_id: Optional[str] = None,
1099
+ scope: str = "branch",
1100
+ k_tokens: int = 4000,
1101
+ ) -> List[dict]:
1102
+ """
1103
+ Query agent reasoning for *concept*, optionally filtered by agent_id.
1104
+ Returns list of ReasoningEntry dicts.
1105
+ """
1106
+ if not self.is_initialized():
1107
+ raise RuntimeError("GCC repository is not initialized.")
1108
+ import dataclasses
1109
+ from .reasoning import ReasoningStore
1110
+ entries = ReasoningStore(self).query(
1111
+ concept=concept, agent_id=agent_id, scope=scope, k_tokens=k_tokens
1112
+ )
1113
+ return [dataclasses.asdict(e) for e in entries]
1114
+
1115
+ # Internal helpers
1116
+
1117
+ def _append_event(self, event_type: str, payload: dict) -> None:
1118
+ def _build_line(current: bytes) -> bytes:
1119
+ prior_hash = _last_event_hash_from_bytes(current)
1120
+ event = {
1121
+ "timestamp": _now_iso(),
1122
+ "actor": "devtorch-cli",
1123
+ "event_type": event_type,
1124
+ "payload": payload,
1125
+ "prior_hash": prior_hash,
1126
+ }
1127
+ event_bytes = json.dumps(event, sort_keys=True).encode("utf-8")
1128
+ event_hash = hashlib.sha256(event_bytes).hexdigest()
1129
+ event["hash"] = event_hash
1130
+ event = sign_line_if_enabled(event, self.gcc_dir)
1131
+ return (json.dumps(event, sort_keys=True) + "\n").encode("utf-8")
1132
+
1133
+ self._backend.read_and_append_atomic(EVENT_LOG_NAME, _build_line)
1134
+
1135
+ def _current_branch(self) -> str:
1136
+ key = f"{REFS_DIR_NAME}/{HEAD_FILE_NAME}"
1137
+ if not self._backend.exists(key):
1138
+ return "main"
1139
+ return self._read(REFS_DIR_NAME, HEAD_FILE_NAME).strip() or "main"
1140
+
1141
+ def _read_branch_tip(self, branch: str) -> Optional[str]:
1142
+ key = f"{REFS_DIR_NAME}/{BRANCHES_DIR_NAME}/{branch}"
1143
+ if not self._backend.exists(key):
1144
+ return None
1145
+ return self._read(REFS_DIR_NAME, BRANCHES_DIR_NAME, branch).strip() or None
1146
+
1147
+ def _write_branch_tip(self, branch: str, commit_id: str) -> None:
1148
+ self._write(REFS_DIR_NAME, BRANCHES_DIR_NAME, branch, content=commit_id)
1149
+
1150
+ def _next_commit_id(self) -> str:
1151
+ """
1152
+ Deterministic, monotonic commit ID that does not depend on wall-clock time.
1153
+ """
1154
+ if self._backend.exists(COMMIT_INDEX_FILE_NAME):
1155
+ raw = self._read(COMMIT_INDEX_FILE_NAME).strip()
1156
+ try:
1157
+ counter = int(raw)
1158
+ except ValueError:
1159
+ counter = 0
1160
+ else:
1161
+ counter = 0
1162
+
1163
+ counter += 1
1164
+ self._write(COMMIT_INDEX_FILE_NAME, content=str(counter))
1165
+
1166
+ # Hex-encode with fixed width for readability
1167
+ return f"{counter:08x}"
1168
+
1169
+
1170
+ def _now_iso() -> str:
1171
+ return _dt.datetime.now(tz=_dt.timezone.utc).isoformat()
1172
+
1173
+
1174
+ def _last_event_hash_from_bytes(content: bytes) -> Optional[str]:
1175
+ """Extract the hash field from the last non-empty JSONL line in content."""
1176
+ last_line: Optional[str] = None
1177
+ try:
1178
+ text = content.decode("utf-8", errors="strict")
1179
+ except UnicodeDecodeError:
1180
+ return None
1181
+ for line in text.splitlines():
1182
+ if line.strip():
1183
+ last_line = line
1184
+ if not last_line:
1185
+ return None
1186
+ try:
1187
+ return json.loads(last_line).get("hash")
1188
+ except json.JSONDecodeError:
1189
+ return None
1190
+
1191
+
1192
+ def _validate_event_log(path: Path) -> List[GCCIssue]:
1193
+ issues: List[GCCIssue] = []
1194
+ last_hash: Optional[str] = None
1195
+ line_no = 0
1196
+
1197
+ with path.open("r", encoding="utf-8") as f:
1198
+ for raw in f:
1199
+ line_no += 1
1200
+ line = raw.strip()
1201
+ if not line:
1202
+ continue
1203
+ try:
1204
+ obj = json.loads(line)
1205
+ except json.JSONDecodeError:
1206
+ issues.append(GCCIssue("event-log", f"Line {line_no}: invalid JSON."))
1207
+ continue
1208
+
1209
+ # Required fields
1210
+ for field in ("timestamp", "actor", "event_type", "payload", "hash"):
1211
+ if field not in obj:
1212
+ issues.append(GCCIssue("event-log", f"Line {line_no}: missing field `{field}`."))
1213
+
1214
+ # Hash chain check
1215
+ prior = obj.get("prior_hash")
1216
+ if prior != last_hash and not (prior is None and last_hash is None):
1217
+ issues.append(
1218
+ GCCIssue(
1219
+ "event-log",
1220
+ f"Line {line_no}: prior_hash mismatch (expected `{last_hash}`, got `{prior}`).",
1221
+ )
1222
+ )
1223
+
1224
+ computed_bytes = json.dumps(
1225
+ {
1226
+ "timestamp": obj.get("timestamp"),
1227
+ "actor": obj.get("actor"),
1228
+ "event_type": obj.get("event_type"),
1229
+ "payload": obj.get("payload"),
1230
+ "prior_hash": obj.get("prior_hash"),
1231
+ },
1232
+ sort_keys=True,
1233
+ ).encode("utf-8")
1234
+ computed_hash = hashlib.sha256(computed_bytes).hexdigest()
1235
+ if obj.get("hash") != computed_hash:
1236
+ issues.append(
1237
+ GCCIssue(
1238
+ "event-log",
1239
+ f"Line {line_no}: hash mismatch (expected `{computed_hash}`, got `{obj.get('hash')}`).",
1240
+ )
1241
+ )
1242
+
1243
+ last_hash = obj.get("hash")
1244
+
1245
+ return issues
1246
+