its-magic 0.1.3-0 → 0.1.3-2
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.
- package/installer.ps1 +8 -0
- package/installer.py +8 -0
- package/installer.sh +1 -0
- package/package.json +1 -1
- package/scripts/check_intake_template_parity.py +62 -0
- package/template/.cursor/agents/curator.mdc +1 -0
- package/template/.cursor/agents/po.mdc +1 -0
- package/template/.cursor/agents/release.mdc +1 -0
- package/template/.cursor/commands/auto.md +90 -0
- package/template/.cursor/commands/execute.md +26 -0
- package/template/.cursor/commands/refresh-context.md +32 -0
- package/template/.cursor/commands/sovereign-critic.md +104 -0
- package/template/.cursor/model-catalog.local.example.cursor-only.json +9 -0
- package/template/.cursor/model-catalog.local.example.json +9 -0
- package/template/.cursor/model-catalog.local.example.level-1-easy.json +9 -0
- package/template/.cursor/model-catalog.local.example.level-2-complex.json +9 -0
- package/template/.cursor/model-catalog.local.example.level-3-mega.json +9 -0
- package/template/.cursor/model-catalog.local.example.level-4-super.json +9 -0
- package/template/.cursor/model-catalog.local.example.role-based-balanced.json +18 -0
- package/template/.cursor/model-catalog.local.example.role-based-highend.json +18 -0
- package/template/.cursor/rules/sovereign-role-manifest.mdc.example +27 -0
- package/template/.cursor/scratchpad.local.example.md +55 -0
- package/template/.cursor/scratchpad.md +203 -0
- package/template/.cursor/sovereign-role-manifest.yaml.example +53 -0
- package/template/decisions/DEC-0104.md +279 -0
- package/template/decisions/DEC-0105.md +231 -0
- package/template/decisions/DEC-0107.md +246 -0
- package/template/docs/engineering/architecture.md +78 -0
- package/template/docs/engineering/auto-orchestration-reference.md +7 -0
- package/template/docs/engineering/context/installer-owned-paths.manifest +8 -0
- package/template/docs/engineering/reason_codes.md +411 -0
- package/template/docs/engineering/runbook.md +1119 -0
- package/template/docs/engineering/sovereign-memory/.gitkeep +0 -0
- package/template/docs/engineering/sovereign-memory/retrospectives/.gitkeep +0 -0
- package/template/handoffs/sovereign_decisions/.gitkeep +0 -0
- package/template/handoffs/sovereign_deferrals/.gitkeep +0 -0
- package/template/handoffs/sovereign_role_reviews.jsonl +0 -0
- package/template/scripts/__pycache__/decision_ledger_lib.cpython-312.pyc +0 -0
- package/template/scripts/check_intake_template_parity.py +181 -0
- package/template/scripts/decision_ledger_lib.py +732 -0
- package/template/scripts/ledger_validate.py +153 -0
- package/template/scripts/model_tier_lib.py +680 -0
- package/template/scripts/model_tier_validate.py +368 -0
- package/template/scripts/parallel_dev_arbiter.py +923 -0
- package/template/scripts/release_trigger_adapters.py +843 -0
- package/template/scripts/self_healing_deploy_lib.py +463 -0
- package/template/scripts/self_healing_deploy_validate.py +78 -0
- package/template/scripts/sovereign_convergence_lib.py +994 -0
- package/template/scripts/sovereign_convergence_validate.py +206 -0
- package/template/scripts/sovereign_critic_lib.py +629 -0
- package/template/scripts/sovereign_critic_validate.py +131 -0
- package/template/scripts/sovereign_loop_lib.py +828 -0
- package/template/scripts/sovereign_loop_validate.py +122 -0
- package/template/scripts/sovereign_memory_lib.py +869 -0
- package/template/scripts/sovereign_memory_validate.py +153 -0
- package/template/scripts/sovereign_role_manifest_lib.py +547 -0
- package/template/scripts/sovereign_role_manifest_validate.py +105 -0
- package/template/tests/us0108_contract_test.py +207 -0
- package/template/tests/us0109_contract_test.py +209 -0
- package/template/tests/us0109_us0110_compose_test.py +34 -0
- package/template/tests/us0111_contract_test.py +345 -0
|
@@ -0,0 +1,828 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Sovereign Loop Mode helper library (US-0107 / DEC-0107).
|
|
4
|
+
|
|
5
|
+
Deferral register, drain-generate gate, notification dispatch, and convergence
|
|
6
|
+
hooks composing US-0088/US-0092/US-0095/US-0103/US-0105/US-0110.
|
|
7
|
+
|
|
8
|
+
Sidecar schema v1 — ``handoffs/sovereign_loop_state.json`` (create-on-first-write):
|
|
9
|
+
|
|
10
|
+
{"schema_version": 1, "drain_generate_iterations": {"<orchestrator_run_id>": <int>}}
|
|
11
|
+
|
|
12
|
+
Reason codes (DEC-0107 §9):
|
|
13
|
+
SOVEREIGN_LOOP_DISABLED, SOVEREIGN_LOOP_GOAL_MODE_REQUIRED,
|
|
14
|
+
SOVEREIGN_DEFERRAL_CAP_EXCEEDED, SOVEREIGN_DEFERRAL_SCHEMA_INVALID,
|
|
15
|
+
SOVEREIGN_DEFERRAL_APPEND_FAILED, SOVEREIGN_DRAIN_GENERATE_CAP,
|
|
16
|
+
SOVEREIGN_DRAIN_GENERATE_BLOCKED, SOVEREIGN_NOTIFY_DISPATCH_FAILED,
|
|
17
|
+
SOVEREIGN_NOTIFY_TARGET_INVALID, SOVEREIGN_NOTIFY_CONFIG_MISSING,
|
|
18
|
+
SOVEREIGN_LOOP_ADVANCE_BLOCKED, DEPLOY_DEFERRED
|
|
19
|
+
|
|
20
|
+
Default-off: AUTO_SOVEREIGN=0 → zero overhead (no reads, no writes, no advance).
|
|
21
|
+
Requires SOVEREIGN_GOAL_MODE=goal_convergence when enabled (fail-closed).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import re
|
|
28
|
+
import sys
|
|
29
|
+
import uuid
|
|
30
|
+
from dataclasses import asdict, dataclass, field
|
|
31
|
+
from datetime import datetime, timezone
|
|
32
|
+
from enum import Enum
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
|
35
|
+
from urllib import error as urllib_error
|
|
36
|
+
from urllib import request as urllib_request
|
|
37
|
+
|
|
38
|
+
_SCRIPT_DIR = Path(__file__).resolve().parent
|
|
39
|
+
if str(_SCRIPT_DIR) not in sys.path:
|
|
40
|
+
sys.path.insert(0, str(_SCRIPT_DIR))
|
|
41
|
+
|
|
42
|
+
from sovereign_convergence_lib import ( # noqa: E402
|
|
43
|
+
SOVEREIGN_GOAL_MODE_KEY,
|
|
44
|
+
check_timeout,
|
|
45
|
+
evaluate_convergence,
|
|
46
|
+
is_goal_convergence_enabled,
|
|
47
|
+
resolve_goal,
|
|
48
|
+
write_partial_delivery_report,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
SCHEMA_VERSION = 1
|
|
52
|
+
DEFERRALS_PATH = Path("handoffs/sovereign_deferrals.jsonl")
|
|
53
|
+
LOOP_STATE_PATH = Path("handoffs/sovereign_loop_state.json")
|
|
54
|
+
|
|
55
|
+
AUTO_SOVEREIGN_KEY = "AUTO_SOVEREIGN"
|
|
56
|
+
AUTO_SOVEREIGN_DEFERRAL_MAX_KEY = "AUTO_SOVEREIGN_DEFERRAL_MAX"
|
|
57
|
+
AUTO_SOVEREIGN_DRAIN_GENERATE_MAX_KEY = "AUTO_SOVEREIGN_DRAIN_GENERATE_MAX"
|
|
58
|
+
AUTO_SOVEREIGN_DEFERRAL_POLICY_KEY = "AUTO_SOVEREIGN_DEFERRAL_POLICY"
|
|
59
|
+
SOVEREIGN_NOTIFY_TARGET_KEY = "SOVEREIGN_NOTIFY_TARGET"
|
|
60
|
+
SOVEREIGN_NOTIFY_NTFY_TOPIC_KEY = "SOVEREIGN_NOTIFY_NTFY_TOPIC"
|
|
61
|
+
SOVEREIGN_NOTIFY_NTFY_BASE_KEY = "SOVEREIGN_NOTIFY_NTFY_BASE"
|
|
62
|
+
SOVEREIGN_NOTIFY_HOOK_URL_KEY = "SOVEREIGN_NOTIFY_HOOK_URL"
|
|
63
|
+
SOVEREIGN_NOTIFY_EMAIL_TO_KEY = "SOVEREIGN_NOTIFY_EMAIL_TO"
|
|
64
|
+
|
|
65
|
+
AUTO_SOVEREIGN_DEFAULT = "0"
|
|
66
|
+
AUTO_SOVEREIGN_DEFERRAL_MAX_DEFAULT = 50
|
|
67
|
+
AUTO_SOVEREIGN_DRAIN_GENERATE_MAX_DEFAULT = 3
|
|
68
|
+
AUTO_SOVEREIGN_DEFERRAL_POLICY_DEFAULT = "resolve_first"
|
|
69
|
+
SOVEREIGN_NOTIFY_TARGET_DEFAULT = "off"
|
|
70
|
+
|
|
71
|
+
AUTO_SOVEREIGN_VALUES = frozenset({"0", "1"})
|
|
72
|
+
DEFERRAL_POLICY_VALUES = frozenset({"stop", "skip", "resolve_first"})
|
|
73
|
+
NOTIFY_TARGET_VALUES = frozenset({"off", "ntfy", "email", "hook"})
|
|
74
|
+
WORK_ITEM_KIND_VALUES = frozenset({"story", "bug", "deploy", "block"})
|
|
75
|
+
DEFERRAL_STATE_VALUES = frozenset({"open", "resolved", "superseded"})
|
|
76
|
+
STEP_ACTION_VALUES = frozenset({
|
|
77
|
+
"noop",
|
|
78
|
+
"continue",
|
|
79
|
+
"defer",
|
|
80
|
+
"drain_generate",
|
|
81
|
+
"terminal_converged",
|
|
82
|
+
"terminal_timeout",
|
|
83
|
+
"terminal_cap",
|
|
84
|
+
"blocked",
|
|
85
|
+
})
|
|
86
|
+
NOTIFY_EVENT_VALUES = frozenset({
|
|
87
|
+
"convergence",
|
|
88
|
+
"timeout",
|
|
89
|
+
"deferral_cap",
|
|
90
|
+
"drain_generate_cap",
|
|
91
|
+
})
|
|
92
|
+
PROVENANCE_VALUES = frozenset({"vision", "memory", "both"})
|
|
93
|
+
|
|
94
|
+
REMEDIATION_HINT_MAX = 512
|
|
95
|
+
WORK_ITEM_REF_MAX = 128
|
|
96
|
+
TITLE_MAX = 120
|
|
97
|
+
SUMMARY_MAX = 512
|
|
98
|
+
AC_SKETCH_MAX_ITEMS = 8
|
|
99
|
+
AC_SKETCH_ITEM_MAX = 256
|
|
100
|
+
MAX_CANDIDATES_PER_ITERATION = 3
|
|
101
|
+
|
|
102
|
+
REQUIRED_DEFERRAL_FIELDS = frozenset({
|
|
103
|
+
"schema_version",
|
|
104
|
+
"deferral_id",
|
|
105
|
+
"ts",
|
|
106
|
+
"reason_code",
|
|
107
|
+
"work_item_kind",
|
|
108
|
+
"work_item_ref",
|
|
109
|
+
"state",
|
|
110
|
+
"source_orchestrator_run_id",
|
|
111
|
+
"remediation_hint",
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
_SECRET_PATTERNS = (
|
|
115
|
+
re.compile(r"api_key\s*=", re.I),
|
|
116
|
+
re.compile(r"-----BEGIN [A-Z ]+PRIVATE KEY-----"),
|
|
117
|
+
re.compile(r"\bsk-[A-Za-z0-9]{8,}\b"),
|
|
118
|
+
re.compile(r"\bghp_[A-Za-z0-9]{20,}\b"),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class ReasonCode(str, Enum):
|
|
123
|
+
SOVEREIGN_LOOP_DISABLED = "SOVEREIGN_LOOP_DISABLED"
|
|
124
|
+
SOVEREIGN_LOOP_GOAL_MODE_REQUIRED = "SOVEREIGN_LOOP_GOAL_MODE_REQUIRED"
|
|
125
|
+
SOVEREIGN_DEFERRAL_CAP_EXCEEDED = "SOVEREIGN_DEFERRAL_CAP_EXCEEDED"
|
|
126
|
+
SOVEREIGN_DEFERRAL_SCHEMA_INVALID = "SOVEREIGN_DEFERRAL_SCHEMA_INVALID"
|
|
127
|
+
SOVEREIGN_DEFERRAL_APPEND_FAILED = "SOVEREIGN_DEFERRAL_APPEND_FAILED"
|
|
128
|
+
SOVEREIGN_DRAIN_GENERATE_CAP = "SOVEREIGN_DRAIN_GENERATE_CAP"
|
|
129
|
+
SOVEREIGN_DRAIN_GENERATE_BLOCKED = "SOVEREIGN_DRAIN_GENERATE_BLOCKED"
|
|
130
|
+
SOVEREIGN_NOTIFY_DISPATCH_FAILED = "SOVEREIGN_NOTIFY_DISPATCH_FAILED"
|
|
131
|
+
SOVEREIGN_NOTIFY_TARGET_INVALID = "SOVEREIGN_NOTIFY_TARGET_INVALID"
|
|
132
|
+
SOVEREIGN_NOTIFY_CONFIG_MISSING = "SOVEREIGN_NOTIFY_CONFIG_MISSING"
|
|
133
|
+
SOVEREIGN_LOOP_ADVANCE_BLOCKED = "SOVEREIGN_LOOP_ADVANCE_BLOCKED"
|
|
134
|
+
DEPLOY_DEFERRED = "DEPLOY_DEFERRED"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
REASON_CODES = frozenset(code.value for code in ReasonCode)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _utc_now_iso() -> str:
|
|
141
|
+
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _scratchpad_int(scratchpad: Dict[str, str], key: str, default: int) -> int:
|
|
145
|
+
raw = scratchpad.get(key, str(default)).strip()
|
|
146
|
+
try:
|
|
147
|
+
return max(0, int(raw))
|
|
148
|
+
except ValueError:
|
|
149
|
+
return default
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def scan_secrets(text: str) -> Optional[ReasonCode]:
|
|
153
|
+
for pattern in _SECRET_PATTERNS:
|
|
154
|
+
if pattern.search(text or ""):
|
|
155
|
+
return ReasonCode.SOVEREIGN_DEFERRAL_SCHEMA_INVALID
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def is_sovereign_loop_enabled(scratchpad: Dict[str, str]) -> bool:
|
|
160
|
+
if scratchpad.get(AUTO_SOVEREIGN_KEY, AUTO_SOVEREIGN_DEFAULT).strip() != "1":
|
|
161
|
+
return False
|
|
162
|
+
return is_goal_convergence_enabled(scratchpad)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def resolve_deferrals_path(repo_root: Path) -> Path:
|
|
166
|
+
return repo_root / DEFERRALS_PATH
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def schema_check_deferral(entry: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
|
|
170
|
+
if not isinstance(entry, dict):
|
|
171
|
+
return False, "entry must be object"
|
|
172
|
+
|
|
173
|
+
missing = REQUIRED_DEFERRAL_FIELDS - set(entry.keys())
|
|
174
|
+
if missing:
|
|
175
|
+
return False, f"missing required fields: {sorted(missing)}"
|
|
176
|
+
|
|
177
|
+
if entry.get("schema_version") != SCHEMA_VERSION:
|
|
178
|
+
return False, "schema_version must be 1"
|
|
179
|
+
|
|
180
|
+
if entry.get("work_item_kind") not in WORK_ITEM_KIND_VALUES:
|
|
181
|
+
return False, "invalid work_item_kind"
|
|
182
|
+
|
|
183
|
+
if entry.get("state") not in DEFERRAL_STATE_VALUES:
|
|
184
|
+
return False, "invalid state"
|
|
185
|
+
|
|
186
|
+
hint = str(entry.get("remediation_hint", ""))
|
|
187
|
+
if not hint or len(hint) > REMEDIATION_HINT_MAX:
|
|
188
|
+
return False, "remediation_hint length invalid"
|
|
189
|
+
|
|
190
|
+
ref = str(entry.get("work_item_ref", ""))
|
|
191
|
+
if not ref or len(ref) > WORK_ITEM_REF_MAX:
|
|
192
|
+
return False, "work_item_ref length invalid"
|
|
193
|
+
|
|
194
|
+
for field_name in ("remediation_hint", "work_item_ref"):
|
|
195
|
+
if scan_secrets(str(entry.get(field_name, ""))):
|
|
196
|
+
return False, ReasonCode.SOVEREIGN_DEFERRAL_SCHEMA_INVALID.value
|
|
197
|
+
|
|
198
|
+
return True, None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def schema_check_drain_generate_candidate(candidate: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
|
|
202
|
+
if not isinstance(candidate, dict):
|
|
203
|
+
return False, "candidate must be object"
|
|
204
|
+
title = str(candidate.get("title", ""))
|
|
205
|
+
summary = str(candidate.get("summary", ""))
|
|
206
|
+
if not title or len(title) > TITLE_MAX:
|
|
207
|
+
return False, "title length invalid"
|
|
208
|
+
if not summary or len(summary) > SUMMARY_MAX:
|
|
209
|
+
return False, "summary length invalid"
|
|
210
|
+
ac_sketch = candidate.get("ac_sketch", [])
|
|
211
|
+
if not isinstance(ac_sketch, list) or len(ac_sketch) > AC_SKETCH_MAX_ITEMS:
|
|
212
|
+
return False, "ac_sketch invalid"
|
|
213
|
+
for item in ac_sketch:
|
|
214
|
+
if len(str(item)) > AC_SKETCH_ITEM_MAX:
|
|
215
|
+
return False, "ac_sketch item too long"
|
|
216
|
+
provenance = candidate.get("provenance", "vision")
|
|
217
|
+
if provenance not in PROVENANCE_VALUES:
|
|
218
|
+
return False, "invalid provenance"
|
|
219
|
+
return True, None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def schema_check_drain_generate_bundle(bundle: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
|
|
223
|
+
if not isinstance(bundle, dict):
|
|
224
|
+
return False, "bundle must be object"
|
|
225
|
+
if bundle.get("schema_version") != SCHEMA_VERSION:
|
|
226
|
+
return False, "schema_version must be 1"
|
|
227
|
+
candidates = bundle.get("candidates", [])
|
|
228
|
+
if not isinstance(candidates, list):
|
|
229
|
+
return False, "candidates must be list"
|
|
230
|
+
if len(candidates) > MAX_CANDIDATES_PER_ITERATION:
|
|
231
|
+
return False, "candidate cap exceeded"
|
|
232
|
+
for candidate in candidates:
|
|
233
|
+
ok, err = schema_check_drain_generate_candidate(candidate)
|
|
234
|
+
if not ok:
|
|
235
|
+
return False, err
|
|
236
|
+
return True, None
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _read_deferral_rows(path: Path) -> List[Dict[str, Any]]:
|
|
240
|
+
if not path.is_file():
|
|
241
|
+
return []
|
|
242
|
+
rows: List[Dict[str, Any]] = []
|
|
243
|
+
for raw in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
244
|
+
if not raw.strip():
|
|
245
|
+
continue
|
|
246
|
+
try:
|
|
247
|
+
obj = json.loads(raw)
|
|
248
|
+
except json.JSONDecodeError:
|
|
249
|
+
continue
|
|
250
|
+
if isinstance(obj, dict):
|
|
251
|
+
rows.append(obj)
|
|
252
|
+
return rows
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def list_open_deferrals(
|
|
256
|
+
repo: Path,
|
|
257
|
+
*,
|
|
258
|
+
scratchpad: Optional[Dict[str, str]] = None,
|
|
259
|
+
) -> Tuple[List[Dict[str, Any]], Optional[str]]:
|
|
260
|
+
if scratchpad is not None and not is_sovereign_loop_enabled(scratchpad):
|
|
261
|
+
return [], ReasonCode.SOVEREIGN_LOOP_DISABLED.value
|
|
262
|
+
|
|
263
|
+
latest: Dict[str, Dict[str, Any]] = {}
|
|
264
|
+
for row in _read_deferral_rows(resolve_deferrals_path(repo)):
|
|
265
|
+
deferral_id = str(row.get("deferral_id", "")).strip()
|
|
266
|
+
if not deferral_id:
|
|
267
|
+
continue
|
|
268
|
+
latest[deferral_id] = row
|
|
269
|
+
|
|
270
|
+
open_rows = [
|
|
271
|
+
row for row in latest.values()
|
|
272
|
+
if str(row.get("state", "")).lower() == "open"
|
|
273
|
+
]
|
|
274
|
+
open_rows.sort(key=lambda r: (str(r.get("ts", "")), str(r.get("deferral_id", ""))))
|
|
275
|
+
return open_rows, None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def build_sample_deferral() -> Dict[str, Any]:
|
|
279
|
+
return {
|
|
280
|
+
"schema_version": SCHEMA_VERSION,
|
|
281
|
+
"deferral_id": str(uuid.uuid4()),
|
|
282
|
+
"ts": _utc_now_iso(),
|
|
283
|
+
"reason_code": ReasonCode.DEPLOY_DEFERRED.value,
|
|
284
|
+
"work_item_kind": "deploy",
|
|
285
|
+
"work_item_ref": "release-target:staging",
|
|
286
|
+
"state": "open",
|
|
287
|
+
"source_orchestrator_run_id": "auto-research-stub",
|
|
288
|
+
"remediation_hint": "Retry deploy smoke after US-0109 ships bounded repair loop.",
|
|
289
|
+
"blocked_by_phase": "release",
|
|
290
|
+
"retry_count": 0,
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@dataclass
|
|
295
|
+
class DrainGenerateCandidate:
|
|
296
|
+
candidate_id: str
|
|
297
|
+
title: str
|
|
298
|
+
summary: str
|
|
299
|
+
ac_sketch: List[str] = field(default_factory=list)
|
|
300
|
+
plan_area_id: Optional[str] = None
|
|
301
|
+
priority: str = "P2"
|
|
302
|
+
provenance: str = "vision"
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@dataclass
|
|
306
|
+
class DrainGenerateCandidateBundle:
|
|
307
|
+
schema_version: int = SCHEMA_VERSION
|
|
308
|
+
orchestrator_run_id: str = ""
|
|
309
|
+
iteration: int = 0
|
|
310
|
+
generated_at: str = ""
|
|
311
|
+
candidates: List[DrainGenerateCandidate] = field(default_factory=list)
|
|
312
|
+
|
|
313
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
314
|
+
payload = asdict(self)
|
|
315
|
+
payload["candidates"] = [asdict(c) for c in self.candidates]
|
|
316
|
+
return payload
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@dataclass
|
|
320
|
+
class SovereignLoopStepResult:
|
|
321
|
+
schema_version: int = SCHEMA_VERSION
|
|
322
|
+
action: str = "noop"
|
|
323
|
+
reason_code: Optional[str] = None
|
|
324
|
+
stop_reason: Optional[str] = None
|
|
325
|
+
orchestrator_run_id: str = ""
|
|
326
|
+
evaluated_at: str = ""
|
|
327
|
+
deferral_id: Optional[str] = None
|
|
328
|
+
drain_generate_bundle: Optional[Dict[str, Any]] = None
|
|
329
|
+
notification_dispatched: bool = False
|
|
330
|
+
convergence: Optional[Dict[str, Any]] = None
|
|
331
|
+
|
|
332
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
333
|
+
return asdict(self)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def append_deferral(
|
|
337
|
+
repo: Path,
|
|
338
|
+
scratchpad: Dict[str, str],
|
|
339
|
+
*,
|
|
340
|
+
reason_code: str,
|
|
341
|
+
work_item_kind: str,
|
|
342
|
+
work_item_ref: str,
|
|
343
|
+
source_orchestrator_run_id: str,
|
|
344
|
+
remediation_hint: str,
|
|
345
|
+
blocked_by_phase: Optional[str] = None,
|
|
346
|
+
retry_count: Optional[int] = None,
|
|
347
|
+
ledger_decision_id: Optional[str] = None,
|
|
348
|
+
) -> Tuple[Optional[str], Optional[str]]:
|
|
349
|
+
if not is_sovereign_loop_enabled(scratchpad):
|
|
350
|
+
return None, ReasonCode.SOVEREIGN_LOOP_DISABLED.value
|
|
351
|
+
|
|
352
|
+
cap = _scratchpad_int(scratchpad, AUTO_SOVEREIGN_DEFERRAL_MAX_KEY, AUTO_SOVEREIGN_DEFERRAL_MAX_DEFAULT)
|
|
353
|
+
open_rows, _ = list_open_deferrals(repo, scratchpad=scratchpad)
|
|
354
|
+
if len(open_rows) >= cap:
|
|
355
|
+
return None, ReasonCode.SOVEREIGN_DEFERRAL_CAP_EXCEEDED.value
|
|
356
|
+
|
|
357
|
+
entry: Dict[str, Any] = {
|
|
358
|
+
"schema_version": SCHEMA_VERSION,
|
|
359
|
+
"deferral_id": str(uuid.uuid4()),
|
|
360
|
+
"ts": _utc_now_iso(),
|
|
361
|
+
"reason_code": reason_code,
|
|
362
|
+
"work_item_kind": work_item_kind,
|
|
363
|
+
"work_item_ref": work_item_ref,
|
|
364
|
+
"state": "open",
|
|
365
|
+
"source_orchestrator_run_id": source_orchestrator_run_id,
|
|
366
|
+
"remediation_hint": remediation_hint,
|
|
367
|
+
}
|
|
368
|
+
if blocked_by_phase:
|
|
369
|
+
entry["blocked_by_phase"] = blocked_by_phase
|
|
370
|
+
if retry_count is not None:
|
|
371
|
+
entry["retry_count"] = retry_count
|
|
372
|
+
if ledger_decision_id:
|
|
373
|
+
entry["ledger_decision_id"] = ledger_decision_id
|
|
374
|
+
|
|
375
|
+
ok, err = schema_check_deferral(entry)
|
|
376
|
+
if not ok:
|
|
377
|
+
return None, err or ReasonCode.SOVEREIGN_DEFERRAL_SCHEMA_INVALID.value
|
|
378
|
+
|
|
379
|
+
path = resolve_deferrals_path(repo)
|
|
380
|
+
try:
|
|
381
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
382
|
+
with path.open("a", encoding="utf-8") as handle:
|
|
383
|
+
handle.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
384
|
+
except OSError:
|
|
385
|
+
return None, ReasonCode.SOVEREIGN_DEFERRAL_APPEND_FAILED.value
|
|
386
|
+
|
|
387
|
+
return str(entry["deferral_id"]), None
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def resolve_deferral(
|
|
391
|
+
repo: Path,
|
|
392
|
+
deferral_id: str,
|
|
393
|
+
*,
|
|
394
|
+
orchestrator_run_id: str,
|
|
395
|
+
) -> Tuple[bool, Optional[str]]:
|
|
396
|
+
rows = _read_deferral_rows(resolve_deferrals_path(repo))
|
|
397
|
+
latest = None
|
|
398
|
+
for row in rows:
|
|
399
|
+
if str(row.get("deferral_id", "")) == deferral_id:
|
|
400
|
+
latest = row
|
|
401
|
+
if latest is None:
|
|
402
|
+
return False, "deferral_id not found"
|
|
403
|
+
if str(latest.get("state", "")).lower() != "open":
|
|
404
|
+
return False, "deferral not open"
|
|
405
|
+
|
|
406
|
+
resolved = dict(latest)
|
|
407
|
+
resolved["ts"] = _utc_now_iso()
|
|
408
|
+
resolved["state"] = "resolved"
|
|
409
|
+
resolved["source_orchestrator_run_id"] = orchestrator_run_id
|
|
410
|
+
|
|
411
|
+
ok, err = schema_check_deferral(resolved)
|
|
412
|
+
if not ok:
|
|
413
|
+
return False, err
|
|
414
|
+
|
|
415
|
+
path = resolve_deferrals_path(repo)
|
|
416
|
+
try:
|
|
417
|
+
with path.open("a", encoding="utf-8") as handle:
|
|
418
|
+
handle.write(json.dumps(resolved, ensure_ascii=False) + "\n")
|
|
419
|
+
except OSError:
|
|
420
|
+
return False, ReasonCode.SOVEREIGN_DEFERRAL_APPEND_FAILED.value
|
|
421
|
+
return True, None
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _read_loop_state(repo: Path) -> Dict[str, Any]:
|
|
425
|
+
state_path = repo / LOOP_STATE_PATH
|
|
426
|
+
if not state_path.is_file():
|
|
427
|
+
return {"schema_version": SCHEMA_VERSION, "drain_generate_iterations": {}}
|
|
428
|
+
try:
|
|
429
|
+
data = json.loads(state_path.read_text(encoding="utf-8"))
|
|
430
|
+
except (OSError, json.JSONDecodeError):
|
|
431
|
+
return {"schema_version": SCHEMA_VERSION, "drain_generate_iterations": {}}
|
|
432
|
+
if not isinstance(data, dict):
|
|
433
|
+
return {"schema_version": SCHEMA_VERSION, "drain_generate_iterations": {}}
|
|
434
|
+
runs = data.get("drain_generate_iterations", {})
|
|
435
|
+
if not isinstance(runs, dict):
|
|
436
|
+
runs = {}
|
|
437
|
+
return {"schema_version": SCHEMA_VERSION, "drain_generate_iterations": runs}
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _write_loop_state(repo: Path, state: Dict[str, Any]) -> None:
|
|
441
|
+
state_path = repo / LOOP_STATE_PATH
|
|
442
|
+
state_path.parent.mkdir(parents=True, exist_ok=True)
|
|
443
|
+
state_path.write_text(json.dumps(state, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def count_drain_generate_iterations(repo: Path, orchestrator_run_id: str) -> int:
|
|
447
|
+
runs = _read_loop_state(repo).get("drain_generate_iterations", {})
|
|
448
|
+
try:
|
|
449
|
+
return int(runs.get(orchestrator_run_id, 0))
|
|
450
|
+
except (TypeError, ValueError):
|
|
451
|
+
return 0
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def increment_drain_generate_iterations(repo: Path, orchestrator_run_id: str) -> int:
|
|
455
|
+
state = _read_loop_state(repo)
|
|
456
|
+
runs = state.setdefault("drain_generate_iterations", {})
|
|
457
|
+
current = count_drain_generate_iterations(repo, orchestrator_run_id)
|
|
458
|
+
new_count = current + 1
|
|
459
|
+
runs[orchestrator_run_id] = new_count
|
|
460
|
+
_write_loop_state(repo, state)
|
|
461
|
+
return new_count
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def build_drain_generate_spawn_inputs(
|
|
465
|
+
repo: Path,
|
|
466
|
+
scratchpad: Dict[str, str],
|
|
467
|
+
convergence: Any,
|
|
468
|
+
) -> Dict[str, Any]:
|
|
469
|
+
goal = resolve_goal(scratchpad, repo)
|
|
470
|
+
inputs: Dict[str, Any] = {
|
|
471
|
+
"vision_path": "docs/product/vision.md",
|
|
472
|
+
"unmet_conditions": getattr(convergence, "unmet_conditions", []),
|
|
473
|
+
"blocked_by": getattr(convergence, "blocked_by", []),
|
|
474
|
+
"goal_text": goal.goal_text or "",
|
|
475
|
+
}
|
|
476
|
+
if scratchpad.get("SOVEREIGN_MEMORY", "0").strip() == "1":
|
|
477
|
+
try:
|
|
478
|
+
from sovereign_memory_lib import build_injection_digest_block # noqa: WPS433
|
|
479
|
+
|
|
480
|
+
block = build_injection_digest_block(scratchpad=scratchpad, repo_root=repo)
|
|
481
|
+
if block:
|
|
482
|
+
inputs["sovereign_memory_digest"] = block
|
|
483
|
+
except ImportError:
|
|
484
|
+
inputs["sovereign_memory_digest"] = None
|
|
485
|
+
return inputs
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def build_drain_generate_ephemeral_id(orchestrator_run_id: str, iteration: int) -> str:
|
|
489
|
+
return f"drain-gen-{orchestrator_run_id}-{iteration}"
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _build_notification_payload(
|
|
493
|
+
*,
|
|
494
|
+
event_type: str,
|
|
495
|
+
orchestrator_run_id: str,
|
|
496
|
+
reason_code: Optional[str],
|
|
497
|
+
convergence: Any = None,
|
|
498
|
+
) -> Dict[str, Any]:
|
|
499
|
+
goal_progress = None
|
|
500
|
+
unmet: List[str] = []
|
|
501
|
+
blocked: List[str] = []
|
|
502
|
+
if convergence is not None:
|
|
503
|
+
if hasattr(convergence, "to_dict"):
|
|
504
|
+
goal_progress = convergence.to_dict()
|
|
505
|
+
unmet = list(getattr(convergence, "unmet_conditions", []))
|
|
506
|
+
blocked = list(getattr(convergence, "blocked_by", []))
|
|
507
|
+
elif isinstance(convergence, dict):
|
|
508
|
+
goal_progress = convergence
|
|
509
|
+
unmet = list(convergence.get("unmet_conditions", []))
|
|
510
|
+
blocked = list(convergence.get("blocked_by", []))
|
|
511
|
+
return {
|
|
512
|
+
"schema_version": SCHEMA_VERSION,
|
|
513
|
+
"event_type": event_type,
|
|
514
|
+
"ts": _utc_now_iso(),
|
|
515
|
+
"orchestrator_run_id": orchestrator_run_id,
|
|
516
|
+
"reason_code": reason_code,
|
|
517
|
+
"unmet_conditions": unmet,
|
|
518
|
+
"blocked_by": blocked,
|
|
519
|
+
"goal_progress": goal_progress,
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def dispatch_notification(
|
|
524
|
+
scratchpad: Dict[str, str],
|
|
525
|
+
event_type: str,
|
|
526
|
+
payload: Dict[str, Any],
|
|
527
|
+
) -> Tuple[bool, Optional[str]]:
|
|
528
|
+
target = scratchpad.get(SOVEREIGN_NOTIFY_TARGET_KEY, SOVEREIGN_NOTIFY_TARGET_DEFAULT).strip().lower()
|
|
529
|
+
if target == "off":
|
|
530
|
+
return True, None
|
|
531
|
+
if event_type not in NOTIFY_EVENT_VALUES:
|
|
532
|
+
return False, ReasonCode.SOVEREIGN_NOTIFY_TARGET_INVALID.value
|
|
533
|
+
if target == "email":
|
|
534
|
+
return False, ReasonCode.SOVEREIGN_NOTIFY_TARGET_INVALID.value
|
|
535
|
+
if target == "ntfy" and not scratchpad.get(SOVEREIGN_NOTIFY_NTFY_TOPIC_KEY, "").strip():
|
|
536
|
+
return True, ReasonCode.SOVEREIGN_NOTIFY_CONFIG_MISSING.value
|
|
537
|
+
if target == "hook" and not scratchpad.get(SOVEREIGN_NOTIFY_HOOK_URL_KEY, "").strip():
|
|
538
|
+
return True, ReasonCode.SOVEREIGN_NOTIFY_CONFIG_MISSING.value
|
|
539
|
+
|
|
540
|
+
try:
|
|
541
|
+
if target == "ntfy":
|
|
542
|
+
topic = scratchpad.get(SOVEREIGN_NOTIFY_NTFY_TOPIC_KEY, "").strip()
|
|
543
|
+
base = scratchpad.get(SOVEREIGN_NOTIFY_NTFY_BASE_KEY, "").strip() or "https://ntfy.sh"
|
|
544
|
+
url = f"{base.rstrip('/')}/{topic}"
|
|
545
|
+
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
|
546
|
+
req = urllib_request.Request(url, data=body, method="POST")
|
|
547
|
+
req.add_header("Title", f"Sovereign {event_type}")
|
|
548
|
+
priority = "4" if event_type in ("timeout", "deferral_cap", "drain_generate_cap") else "3"
|
|
549
|
+
req.add_header("Priority", priority)
|
|
550
|
+
req.add_header("Tags", "sovereign,auto")
|
|
551
|
+
urllib_request.urlopen(req, timeout=10)
|
|
552
|
+
elif target == "hook":
|
|
553
|
+
hook_url = scratchpad.get(SOVEREIGN_NOTIFY_HOOK_URL_KEY, "").strip()
|
|
554
|
+
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
|
555
|
+
req = urllib_request.Request(hook_url, data=body, method="POST")
|
|
556
|
+
req.add_header("Content-Type", "application/json")
|
|
557
|
+
urllib_request.urlopen(req, timeout=10)
|
|
558
|
+
except (OSError, urllib_error.URLError, urllib_error.HTTPError, ValueError):
|
|
559
|
+
print(
|
|
560
|
+
f"[{ReasonCode.SOVEREIGN_NOTIFY_DISPATCH_FAILED.value}] adapter error for {event_type}",
|
|
561
|
+
file=sys.stderr,
|
|
562
|
+
)
|
|
563
|
+
return True, None
|
|
564
|
+
|
|
565
|
+
return True, None
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def advance_sovereign_loop(
|
|
569
|
+
repo: Path,
|
|
570
|
+
scratchpad: Dict[str, str],
|
|
571
|
+
*,
|
|
572
|
+
orchestrator_run_id: str,
|
|
573
|
+
iteration: Optional[int] = None,
|
|
574
|
+
) -> SovereignLoopStepResult:
|
|
575
|
+
evaluated_at = _utc_now_iso()
|
|
576
|
+
if scratchpad.get(AUTO_SOVEREIGN_KEY, AUTO_SOVEREIGN_DEFAULT).strip() != "1":
|
|
577
|
+
return SovereignLoopStepResult(
|
|
578
|
+
action="noop",
|
|
579
|
+
orchestrator_run_id=orchestrator_run_id,
|
|
580
|
+
evaluated_at=evaluated_at,
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
if not is_goal_convergence_enabled(scratchpad):
|
|
584
|
+
return SovereignLoopStepResult(
|
|
585
|
+
action="blocked",
|
|
586
|
+
reason_code=ReasonCode.SOVEREIGN_LOOP_GOAL_MODE_REQUIRED.value,
|
|
587
|
+
stop_reason=ReasonCode.SOVEREIGN_LOOP_GOAL_MODE_REQUIRED.value,
|
|
588
|
+
orchestrator_run_id=orchestrator_run_id,
|
|
589
|
+
evaluated_at=evaluated_at,
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
iter_count = iteration if iteration is not None else 0
|
|
593
|
+
convergence = evaluate_convergence(
|
|
594
|
+
repo,
|
|
595
|
+
scratchpad,
|
|
596
|
+
orchestrator_run_id=orchestrator_run_id,
|
|
597
|
+
iteration=iteration,
|
|
598
|
+
)
|
|
599
|
+
conv_dict = convergence.to_dict()
|
|
600
|
+
|
|
601
|
+
if convergence.converged:
|
|
602
|
+
payload = _build_notification_payload(
|
|
603
|
+
event_type="convergence",
|
|
604
|
+
orchestrator_run_id=orchestrator_run_id,
|
|
605
|
+
reason_code="converged",
|
|
606
|
+
convergence=convergence,
|
|
607
|
+
)
|
|
608
|
+
_, _ = dispatch_notification(scratchpad, "convergence", payload)
|
|
609
|
+
return SovereignLoopStepResult(
|
|
610
|
+
action="terminal_converged",
|
|
611
|
+
stop_reason="converged",
|
|
612
|
+
orchestrator_run_id=orchestrator_run_id,
|
|
613
|
+
evaluated_at=evaluated_at,
|
|
614
|
+
notification_dispatched=True,
|
|
615
|
+
convergence=conv_dict,
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
timed_out, timeout_reason = check_timeout(scratchpad, iter_count)
|
|
619
|
+
if timed_out and timeout_reason is not None:
|
|
620
|
+
goal = resolve_goal(scratchpad, repo)
|
|
621
|
+
write_partial_delivery_report(
|
|
622
|
+
repo,
|
|
623
|
+
convergence,
|
|
624
|
+
goal.goal_text or "",
|
|
625
|
+
timeout_reason.value,
|
|
626
|
+
orchestrator_run_id,
|
|
627
|
+
)
|
|
628
|
+
payload = _build_notification_payload(
|
|
629
|
+
event_type="timeout",
|
|
630
|
+
orchestrator_run_id=orchestrator_run_id,
|
|
631
|
+
reason_code=timeout_reason.value,
|
|
632
|
+
convergence=convergence,
|
|
633
|
+
)
|
|
634
|
+
_, _ = dispatch_notification(scratchpad, "timeout", payload)
|
|
635
|
+
return SovereignLoopStepResult(
|
|
636
|
+
action="terminal_timeout",
|
|
637
|
+
reason_code=timeout_reason.value,
|
|
638
|
+
stop_reason=timeout_reason.value,
|
|
639
|
+
orchestrator_run_id=orchestrator_run_id,
|
|
640
|
+
evaluated_at=evaluated_at,
|
|
641
|
+
notification_dispatched=True,
|
|
642
|
+
convergence=conv_dict,
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
open_deferrals, _ = list_open_deferrals(repo, scratchpad=scratchpad)
|
|
646
|
+
policy = scratchpad.get(
|
|
647
|
+
AUTO_SOVEREIGN_DEFERRAL_POLICY_KEY,
|
|
648
|
+
AUTO_SOVEREIGN_DEFERRAL_POLICY_DEFAULT,
|
|
649
|
+
).strip()
|
|
650
|
+
|
|
651
|
+
deferral_cap = _scratchpad_int(
|
|
652
|
+
scratchpad,
|
|
653
|
+
AUTO_SOVEREIGN_DEFERRAL_MAX_KEY,
|
|
654
|
+
AUTO_SOVEREIGN_DEFERRAL_MAX_DEFAULT,
|
|
655
|
+
)
|
|
656
|
+
if len(open_deferrals) >= deferral_cap:
|
|
657
|
+
payload = _build_notification_payload(
|
|
658
|
+
event_type="deferral_cap",
|
|
659
|
+
orchestrator_run_id=orchestrator_run_id,
|
|
660
|
+
reason_code=ReasonCode.SOVEREIGN_DEFERRAL_CAP_EXCEEDED.value,
|
|
661
|
+
convergence=convergence,
|
|
662
|
+
)
|
|
663
|
+
_, _ = dispatch_notification(scratchpad, "deferral_cap", payload)
|
|
664
|
+
return SovereignLoopStepResult(
|
|
665
|
+
action="terminal_cap",
|
|
666
|
+
reason_code=ReasonCode.SOVEREIGN_DEFERRAL_CAP_EXCEEDED.value,
|
|
667
|
+
stop_reason=ReasonCode.SOVEREIGN_DEFERRAL_CAP_EXCEEDED.value,
|
|
668
|
+
orchestrator_run_id=orchestrator_run_id,
|
|
669
|
+
evaluated_at=evaluated_at,
|
|
670
|
+
notification_dispatched=True,
|
|
671
|
+
convergence=conv_dict,
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
if open_deferrals and policy == "stop":
|
|
675
|
+
return SovereignLoopStepResult(
|
|
676
|
+
action="defer",
|
|
677
|
+
reason_code=ReasonCode.SOVEREIGN_LOOP_ADVANCE_BLOCKED.value,
|
|
678
|
+
deferral_id=str(open_deferrals[0].get("deferral_id", "")),
|
|
679
|
+
orchestrator_run_id=orchestrator_run_id,
|
|
680
|
+
evaluated_at=evaluated_at,
|
|
681
|
+
convergence=conv_dict,
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
if open_deferrals and policy == "resolve_first":
|
|
685
|
+
return SovereignLoopStepResult(
|
|
686
|
+
action="blocked",
|
|
687
|
+
reason_code=ReasonCode.SOVEREIGN_LOOP_ADVANCE_BLOCKED.value,
|
|
688
|
+
stop_reason=ReasonCode.SOVEREIGN_LOOP_ADVANCE_BLOCKED.value,
|
|
689
|
+
orchestrator_run_id=orchestrator_run_id,
|
|
690
|
+
evaluated_at=evaluated_at,
|
|
691
|
+
convergence=conv_dict,
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
backlog_conj = convergence.conjuncts.get("backlog_clear")
|
|
695
|
+
if backlog_conj and backlog_conj.status == "fail":
|
|
696
|
+
return SovereignLoopStepResult(
|
|
697
|
+
action="continue",
|
|
698
|
+
orchestrator_run_id=orchestrator_run_id,
|
|
699
|
+
evaluated_at=evaluated_at,
|
|
700
|
+
convergence=conv_dict,
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
drain_max = _scratchpad_int(
|
|
704
|
+
scratchpad,
|
|
705
|
+
AUTO_SOVEREIGN_DRAIN_GENERATE_MAX_KEY,
|
|
706
|
+
AUTO_SOVEREIGN_DRAIN_GENERATE_MAX_DEFAULT,
|
|
707
|
+
)
|
|
708
|
+
current_iter = count_drain_generate_iterations(repo, orchestrator_run_id)
|
|
709
|
+
if current_iter < drain_max:
|
|
710
|
+
new_iter = increment_drain_generate_iterations(repo, orchestrator_run_id)
|
|
711
|
+
bundle = DrainGenerateCandidateBundle(
|
|
712
|
+
orchestrator_run_id=orchestrator_run_id,
|
|
713
|
+
iteration=new_iter,
|
|
714
|
+
generated_at=evaluated_at,
|
|
715
|
+
candidates=[],
|
|
716
|
+
)
|
|
717
|
+
return SovereignLoopStepResult(
|
|
718
|
+
action="drain_generate",
|
|
719
|
+
orchestrator_run_id=orchestrator_run_id,
|
|
720
|
+
evaluated_at=evaluated_at,
|
|
721
|
+
drain_generate_bundle=bundle.to_dict(),
|
|
722
|
+
convergence=conv_dict,
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
payload = _build_notification_payload(
|
|
726
|
+
event_type="drain_generate_cap",
|
|
727
|
+
orchestrator_run_id=orchestrator_run_id,
|
|
728
|
+
reason_code=ReasonCode.SOVEREIGN_DRAIN_GENERATE_CAP.value,
|
|
729
|
+
convergence=convergence,
|
|
730
|
+
)
|
|
731
|
+
_, _ = dispatch_notification(scratchpad, "drain_generate_cap", payload)
|
|
732
|
+
return SovereignLoopStepResult(
|
|
733
|
+
action="terminal_cap",
|
|
734
|
+
reason_code=ReasonCode.SOVEREIGN_DRAIN_GENERATE_CAP.value,
|
|
735
|
+
stop_reason=ReasonCode.SOVEREIGN_DRAIN_GENERATE_CAP.value,
|
|
736
|
+
orchestrator_run_id=orchestrator_run_id,
|
|
737
|
+
evaluated_at=evaluated_at,
|
|
738
|
+
notification_dispatched=True,
|
|
739
|
+
convergence=conv_dict,
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def self_test() -> bool:
|
|
744
|
+
errors: List[str] = []
|
|
745
|
+
|
|
746
|
+
if is_sovereign_loop_enabled({AUTO_SOVEREIGN_KEY: "0", SOVEREIGN_GOAL_MODE_KEY: "goal_convergence"}):
|
|
747
|
+
errors.append("AUTO_SOVEREIGN=0 must disable even with goal_convergence")
|
|
748
|
+
|
|
749
|
+
if not is_sovereign_loop_enabled({AUTO_SOVEREIGN_KEY: "1", SOVEREIGN_GOAL_MODE_KEY: "goal_convergence"}):
|
|
750
|
+
errors.append("AUTO_SOVEREIGN=1 + goal_convergence must enable")
|
|
751
|
+
|
|
752
|
+
if is_sovereign_loop_enabled({AUTO_SOVEREIGN_KEY: "1", SOVEREIGN_GOAL_MODE_KEY: "phase_driven"}):
|
|
753
|
+
errors.append("phase_driven must fail-closed for sovereign loop")
|
|
754
|
+
|
|
755
|
+
sample = build_sample_deferral()
|
|
756
|
+
ok, err = schema_check_deferral(sample)
|
|
757
|
+
if not ok:
|
|
758
|
+
errors.append(f"sample deferral invalid: {err}")
|
|
759
|
+
|
|
760
|
+
bad_secret = build_sample_deferral()
|
|
761
|
+
bad_secret["remediation_hint"] = "api_key=leaked"
|
|
762
|
+
bad_ok, bad_err = schema_check_deferral(bad_secret)
|
|
763
|
+
if bad_ok:
|
|
764
|
+
errors.append("secret scan must reject api_key patterns")
|
|
765
|
+
|
|
766
|
+
step = advance_sovereign_loop(
|
|
767
|
+
Path("."),
|
|
768
|
+
{AUTO_SOVEREIGN_KEY: "0"},
|
|
769
|
+
orchestrator_run_id="self-test",
|
|
770
|
+
)
|
|
771
|
+
if step.action != "noop":
|
|
772
|
+
errors.append("disabled advance must noop")
|
|
773
|
+
|
|
774
|
+
blocked = advance_sovereign_loop(
|
|
775
|
+
Path("."),
|
|
776
|
+
{AUTO_SOVEREIGN_KEY: "1", SOVEREIGN_GOAL_MODE_KEY: "phase_driven"},
|
|
777
|
+
orchestrator_run_id="self-test",
|
|
778
|
+
)
|
|
779
|
+
if blocked.action != "blocked" or blocked.reason_code != ReasonCode.SOVEREIGN_LOOP_GOAL_MODE_REQUIRED.value:
|
|
780
|
+
errors.append("goal mode coupling must fail-closed")
|
|
781
|
+
|
|
782
|
+
notify_ok, _ = dispatch_notification(
|
|
783
|
+
{SOVEREIGN_NOTIFY_TARGET_KEY: "off"},
|
|
784
|
+
"convergence",
|
|
785
|
+
{"schema_version": 1},
|
|
786
|
+
)
|
|
787
|
+
if not notify_ok:
|
|
788
|
+
errors.append("off target must fail-open success")
|
|
789
|
+
|
|
790
|
+
email_ok, email_code = dispatch_notification(
|
|
791
|
+
{SOVEREIGN_NOTIFY_TARGET_KEY: "email"},
|
|
792
|
+
"convergence",
|
|
793
|
+
{"schema_version": 1},
|
|
794
|
+
)
|
|
795
|
+
if email_ok or email_code != ReasonCode.SOVEREIGN_NOTIFY_TARGET_INVALID.value:
|
|
796
|
+
errors.append("email target must return SOVEREIGN_NOTIFY_TARGET_INVALID")
|
|
797
|
+
|
|
798
|
+
bundle = DrainGenerateCandidateBundle(
|
|
799
|
+
orchestrator_run_id="self-test",
|
|
800
|
+
iteration=1,
|
|
801
|
+
generated_at=_utc_now_iso(),
|
|
802
|
+
candidates=[
|
|
803
|
+
DrainGenerateCandidate(
|
|
804
|
+
candidate_id=str(uuid.uuid4()),
|
|
805
|
+
title="Example follow-on story",
|
|
806
|
+
summary="Bounded drain-generate candidate for self-test.",
|
|
807
|
+
ac_sketch=["AC-1: example"],
|
|
808
|
+
),
|
|
809
|
+
],
|
|
810
|
+
)
|
|
811
|
+
bundle_ok, bundle_err = schema_check_drain_generate_bundle(bundle.to_dict())
|
|
812
|
+
if not bundle_ok:
|
|
813
|
+
errors.append(f"bundle schema failed: {bundle_err}")
|
|
814
|
+
|
|
815
|
+
if len(REASON_CODES) != 12:
|
|
816
|
+
errors.append("reason code inventory must be 12 codes")
|
|
817
|
+
|
|
818
|
+
if errors:
|
|
819
|
+
for msg in errors:
|
|
820
|
+
print(f"[SOVEREIGN_LOOP_SELF_TEST_FAIL] {msg}", file=sys.stderr)
|
|
821
|
+
return False
|
|
822
|
+
|
|
823
|
+
print("[SOVEREIGN_LOOP_SELF_TEST_OK]")
|
|
824
|
+
return True
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
if __name__ == "__main__":
|
|
828
|
+
raise SystemExit(0 if self_test() else 1)
|