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
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
|
+
|