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.
Files changed (61) hide show
  1. package/installer.ps1 +8 -0
  2. package/installer.py +8 -0
  3. package/installer.sh +1 -0
  4. package/package.json +1 -1
  5. package/scripts/check_intake_template_parity.py +62 -0
  6. package/template/.cursor/agents/curator.mdc +1 -0
  7. package/template/.cursor/agents/po.mdc +1 -0
  8. package/template/.cursor/agents/release.mdc +1 -0
  9. package/template/.cursor/commands/auto.md +90 -0
  10. package/template/.cursor/commands/execute.md +26 -0
  11. package/template/.cursor/commands/refresh-context.md +32 -0
  12. package/template/.cursor/commands/sovereign-critic.md +104 -0
  13. package/template/.cursor/model-catalog.local.example.cursor-only.json +9 -0
  14. package/template/.cursor/model-catalog.local.example.json +9 -0
  15. package/template/.cursor/model-catalog.local.example.level-1-easy.json +9 -0
  16. package/template/.cursor/model-catalog.local.example.level-2-complex.json +9 -0
  17. package/template/.cursor/model-catalog.local.example.level-3-mega.json +9 -0
  18. package/template/.cursor/model-catalog.local.example.level-4-super.json +9 -0
  19. package/template/.cursor/model-catalog.local.example.role-based-balanced.json +18 -0
  20. package/template/.cursor/model-catalog.local.example.role-based-highend.json +18 -0
  21. package/template/.cursor/rules/sovereign-role-manifest.mdc.example +27 -0
  22. package/template/.cursor/scratchpad.local.example.md +55 -0
  23. package/template/.cursor/scratchpad.md +203 -0
  24. package/template/.cursor/sovereign-role-manifest.yaml.example +53 -0
  25. package/template/decisions/DEC-0104.md +279 -0
  26. package/template/decisions/DEC-0105.md +231 -0
  27. package/template/decisions/DEC-0107.md +246 -0
  28. package/template/docs/engineering/architecture.md +78 -0
  29. package/template/docs/engineering/auto-orchestration-reference.md +7 -0
  30. package/template/docs/engineering/context/installer-owned-paths.manifest +8 -0
  31. package/template/docs/engineering/reason_codes.md +411 -0
  32. package/template/docs/engineering/runbook.md +1119 -0
  33. package/template/docs/engineering/sovereign-memory/.gitkeep +0 -0
  34. package/template/docs/engineering/sovereign-memory/retrospectives/.gitkeep +0 -0
  35. package/template/handoffs/sovereign_decisions/.gitkeep +0 -0
  36. package/template/handoffs/sovereign_deferrals/.gitkeep +0 -0
  37. package/template/handoffs/sovereign_role_reviews.jsonl +0 -0
  38. package/template/scripts/__pycache__/decision_ledger_lib.cpython-312.pyc +0 -0
  39. package/template/scripts/check_intake_template_parity.py +181 -0
  40. package/template/scripts/decision_ledger_lib.py +732 -0
  41. package/template/scripts/ledger_validate.py +153 -0
  42. package/template/scripts/model_tier_lib.py +680 -0
  43. package/template/scripts/model_tier_validate.py +368 -0
  44. package/template/scripts/parallel_dev_arbiter.py +923 -0
  45. package/template/scripts/release_trigger_adapters.py +843 -0
  46. package/template/scripts/self_healing_deploy_lib.py +463 -0
  47. package/template/scripts/self_healing_deploy_validate.py +78 -0
  48. package/template/scripts/sovereign_convergence_lib.py +994 -0
  49. package/template/scripts/sovereign_convergence_validate.py +206 -0
  50. package/template/scripts/sovereign_critic_lib.py +629 -0
  51. package/template/scripts/sovereign_critic_validate.py +131 -0
  52. package/template/scripts/sovereign_loop_lib.py +828 -0
  53. package/template/scripts/sovereign_loop_validate.py +122 -0
  54. package/template/scripts/sovereign_memory_lib.py +869 -0
  55. package/template/scripts/sovereign_memory_validate.py +153 -0
  56. package/template/scripts/sovereign_role_manifest_lib.py +547 -0
  57. package/template/scripts/sovereign_role_manifest_validate.py +105 -0
  58. package/template/tests/us0108_contract_test.py +207 -0
  59. package/template/tests/us0109_contract_test.py +209 -0
  60. package/template/tests/us0109_us0110_compose_test.py +34 -0
  61. package/template/tests/us0111_contract_test.py +345 -0
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Sovereign Memory JSONL validator (US-0105 / DEC-0105).
4
+
5
+ Validates docs/engineering/sovereign-memory/*.jsonl entries against v1 family schemas.
6
+
7
+ Exit codes:
8
+ - 0: validation passed (or --self-test OK)
9
+ - 1: fail-closed validation error (with --enforce)
10
+ - 2: usage error
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import sys
18
+ from pathlib import Path
19
+ from typing import List, Optional, Tuple
20
+
21
+ _SCRIPT_DIR = Path(__file__).resolve().parent
22
+ sys.path.insert(0, str(_SCRIPT_DIR))
23
+
24
+ from sovereign_memory_lib import ( # noqa: E402
25
+ JSONL_FAMILIES,
26
+ JSONL_FILENAMES,
27
+ ReasonCode,
28
+ resolve_jsonl_path,
29
+ schema_check,
30
+ self_test,
31
+ )
32
+
33
+
34
+ def validate_jsonl_file(path: Path, family: str) -> Tuple[bool, List[str], List[str]]:
35
+ errors: List[str] = []
36
+ codes: List[str] = []
37
+ if not path.is_file():
38
+ return True, errors, codes
39
+
40
+ for line_no, raw in enumerate(path.read_text(encoding="utf-8", errors="replace").splitlines(), start=1):
41
+ if not raw.strip():
42
+ continue
43
+ try:
44
+ obj = json.loads(raw)
45
+ except json.JSONDecodeError as exc:
46
+ errors.append(f"{path}:{line_no}: JSON decode error: {exc}")
47
+ codes.append(ReasonCode.SOVEREIGN_MEMORY_SCHEMA_INVALID.value)
48
+ continue
49
+ ok, err = schema_check(obj, family)
50
+ if not ok:
51
+ errors.append(f"{path}:{line_no}: {err}")
52
+ codes.append(ReasonCode.SOVEREIGN_MEMORY_SCHEMA_INVALID.value)
53
+
54
+ return len(errors) == 0, errors, codes
55
+
56
+
57
+ def validate_repo(repo: Path, family: str) -> Tuple[bool, List[str]]:
58
+ errors: List[str] = []
59
+ families = JSONL_FAMILIES if family == "all" else {family}
60
+
61
+ for fam in sorted(families):
62
+ path = resolve_jsonl_path(fam, repo)
63
+ if not path.is_file():
64
+ continue
65
+ ok, file_errors, _ = validate_jsonl_file(path, fam)
66
+ if not ok:
67
+ errors.extend(file_errors)
68
+ return len(errors) == 0, errors
69
+
70
+
71
+ def run_self_test() -> bool:
72
+ if not self_test():
73
+ return False
74
+
75
+ import tempfile
76
+ import uuid
77
+
78
+ from sovereign_memory_lib import build_sample_decision # noqa: WPS433
79
+
80
+ good = build_sample_decision()
81
+ good["entry_id"] = str(uuid.uuid4())
82
+ ok, err = schema_check(good, "decisions")
83
+ if not ok:
84
+ print(f" fixture decision: {err}", file=sys.stderr)
85
+ return False
86
+
87
+ with tempfile.TemporaryDirectory() as tmp:
88
+ bad_path = Path(tmp) / "bad.jsonl"
89
+ bad_path.write_text('{"schema_version": 1}\n', encoding="utf-8")
90
+ ok_file, errors, _ = validate_jsonl_file(bad_path, "decisions")
91
+ if ok_file or not errors:
92
+ print(" expected invalid fixture to fail", file=sys.stderr)
93
+ return False
94
+
95
+ print("[SOVEREIGN_MEMORY_VALIDATION_OK]")
96
+ return True
97
+
98
+
99
+ def main() -> int:
100
+ parser = argparse.ArgumentParser(description="Sovereign memory validator (US-0105 / DEC-0105)")
101
+ parser.add_argument("--file", type=Path, help="Validate single JSONL file")
102
+ parser.add_argument("--repo", type=Path, help="Validate sovereign-memory JSONL if present")
103
+ parser.add_argument(
104
+ "--family",
105
+ choices=sorted(JSONL_FAMILIES | {"all"}),
106
+ default="all",
107
+ help="JSONL family to validate (default all)",
108
+ )
109
+ parser.add_argument("--self-test", action="store_true", help="Run lib self-test + schema fixtures")
110
+ parser.add_argument("--enforce", action="store_true", help="Exit non-zero on validation failure")
111
+
112
+ args = parser.parse_args()
113
+
114
+ if args.self_test:
115
+ return 0 if run_self_test() else 1
116
+
117
+ errors: List[str] = []
118
+
119
+ if args.file is not None:
120
+ family = args.family if args.family != "all" else "decisions"
121
+ if args.family == "all":
122
+ matched = None
123
+ for fam, name in JSONL_FILENAMES.items():
124
+ if args.file.name == name:
125
+ matched = fam
126
+ break
127
+ family = matched or "decisions"
128
+ ok, file_errors, _ = validate_jsonl_file(args.file, family)
129
+ if not ok:
130
+ errors.extend(file_errors)
131
+
132
+ if args.repo is not None:
133
+ ok_repo, repo_errors = validate_repo(args.repo, args.family)
134
+ if not ok_repo:
135
+ errors.extend(repo_errors)
136
+
137
+ if args.file is None and args.repo is None:
138
+ parser.print_help()
139
+ return 2
140
+
141
+ if errors:
142
+ for item in errors:
143
+ print(item, file=sys.stderr)
144
+ if args.enforce:
145
+ return 1
146
+ return 0
147
+
148
+ print("[SOVEREIGN_MEMORY_VALIDATION_OK]")
149
+ return 0
150
+
151
+
152
+ if __name__ == "__main__":
153
+ sys.exit(main())
@@ -0,0 +1,547 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Sovereign Role-Behavior Manifest helper library (US-0106 / DEC-0106).
4
+
5
+ Additive per-role objective + inter-role review obligations layer on top of
6
+ US-0069 spawn machinery. Review spawns are supplementary post-phase hooks —
7
+ they never substitute for the US-0069 producer role.
8
+
9
+ Reason codes (DEC-0106 §10):
10
+ SOVEREIGN_ROLE_MANIFEST_DISABLED, SOVEREIGN_ROLE_MANIFEST_SCHEMA_INVALID,
11
+ SOVEREIGN_ROLE_UNKNOWN_ROLE, SOVEREIGN_ROLE_UNKNOWN_PHASE,
12
+ SOVEREIGN_ROLE_SECRET_DETECTED, SOVEREIGN_ROLE_OBJECTIVE_OVERFLOW,
13
+ ROLE_REVIEW_DISPATCH_FAILED, ROLE_REVIEW_SPAWN_FAILED,
14
+ ROLE_REVIEW_BLOCKED, ROLE_REVIEW_DEFERRAL_FAILED,
15
+ ROLE_REVIEW_REWORK_CAP
16
+
17
+ Default-off: SOVEREIGN_ROLE_MANIFEST=0 → zero overhead.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import hashlib
23
+ import json
24
+ import os
25
+ import re
26
+ import sys
27
+ from dataclasses import dataclass, field
28
+ from datetime import datetime, timezone
29
+ from enum import Enum
30
+ from pathlib import Path
31
+ from typing import Any, Dict, List, Optional, Sequence, Tuple
32
+
33
+ SCHEMA_VERSION = 1
34
+ MANIFEST_REL = ".cursor/sovereign-role-manifest.yaml"
35
+ REVIEWS_JSONL_REL = "handoffs/sovereign_role_reviews.jsonl"
36
+
37
+ SOVEREIGN_ROLE_MANIFEST_KEY = "SOVEREIGN_ROLE_MANIFEST"
38
+ SOVEREIGN_ROLE_OBJECTIVE_MAX_CHARS_KEY = "SOVEREIGN_ROLE_OBJECTIVE_MAX_CHARS"
39
+ SOVEREIGN_ROLE_REVIEW_MAX_PER_PHASE_KEY = "SOVEREIGN_ROLE_REVIEW_MAX_PER_PHASE"
40
+ SOVEREIGN_ROLE_REVIEW_REWORK_MAX_KEY = "SOVEREIGN_ROLE_REVIEW_REWORK_MAX"
41
+
42
+ SOVEREIGN_ROLE_MANIFEST_VALUES = frozenset({"0", "1"})
43
+ SOVEREIGN_ROLE_MANIFEST_DEFAULT = "0"
44
+ SOVEREIGN_ROLE_OBJECTIVE_MAX_CHARS_DEFAULT = 512
45
+ SOVEREIGN_ROLE_REVIEW_MAX_PER_PHASE_DEFAULT = 2
46
+ SOVEREIGN_ROLE_REVIEW_REWORK_MAX_DEFAULT = 1
47
+
48
+ OBJECTIVE_FILE_MAX_CHARS = 1024
49
+
50
+ VALID_ROLE_IDS = frozenset({"po", "tech-lead", "dev", "qa", "release", "curator"})
51
+
52
+ VALID_REVIEW_FOCI = frozenset({
53
+ "user_value_drift",
54
+ "testability",
55
+ "buildability",
56
+ "deployability",
57
+ })
58
+
59
+ VALID_DEFAULT_ORDERS = frozenset({
60
+ "role_review_first",
61
+ "critic_first",
62
+ "critic_only",
63
+ "role_review_only",
64
+ })
65
+
66
+ ALLOWED_SELF_OVERRIDES = frozenset({"verbosity", "detail_level", "tone"})
67
+
68
+ VALID_TRIGGER_PHASES = frozenset({
69
+ "intake",
70
+ "discovery",
71
+ "research",
72
+ "architecture",
73
+ "plan-verify",
74
+ "execute",
75
+ "qa",
76
+ "verify-work",
77
+ "release",
78
+ "refresh-context",
79
+ "pause",
80
+ "security-review",
81
+ })
82
+
83
+
84
+ class ReasonCode(str, Enum):
85
+ DISABLED = "SOVEREIGN_ROLE_MANIFEST_DISABLED"
86
+ SCHEMA_INVALID = "SOVEREIGN_ROLE_MANIFEST_SCHEMA_INVALID"
87
+ UNKNOWN_ROLE = "SOVEREIGN_ROLE_UNKNOWN_ROLE"
88
+ UNKNOWN_PHASE = "SOVEREIGN_ROLE_UNKNOWN_PHASE"
89
+ SECRET_DETECTED = "SOVEREIGN_ROLE_SECRET_DETECTED"
90
+ OBJECTIVE_OVERFLOW = "SOVEREIGN_ROLE_OBJECTIVE_OVERFLOW"
91
+ REVIEW_DISPATCH_FAILED = "ROLE_REVIEW_DISPATCH_FAILED"
92
+ REVIEW_SPAWN_FAILED = "ROLE_REVIEW_SPAWN_FAILED"
93
+ REVIEW_BLOCKED = "ROLE_REVIEW_BLOCKED"
94
+ REVIEW_DEFERRAL_FAILED = "ROLE_REVIEW_DEFERRAL_FAILED"
95
+ REVIEW_REWORK_CAP = "ROLE_REVIEW_REWORK_CAP"
96
+
97
+
98
+ SECRET_PATTERNS = [
99
+ re.compile(r"(?i)(api[_-]?key|token|password|secret|private[_-]?key)\s*[:=]\s*\S+"),
100
+ re.compile(r"sk-[a-zA-Z0-9]{16,}"),
101
+ re.compile(r"ghp_[a-zA-Z0-9]{36}"),
102
+ re.compile(r"(?i)bearer\s+[a-zA-Z0-9\-._~+/]+=*"),
103
+ ]
104
+
105
+
106
+ def is_role_manifest_enabled(scratchpad: Dict[str, Any]) -> bool:
107
+ return str(scratchpad.get(SOVEREIGN_ROLE_MANIFEST_KEY, SOVEREIGN_ROLE_MANIFEST_DEFAULT)) == "1"
108
+
109
+
110
+ def get_objective_max_chars(scratchpad: Dict[str, Any]) -> int:
111
+ raw = scratchpad.get(SOVEREIGN_ROLE_OBJECTIVE_MAX_CHARS_KEY, SOVEREIGN_ROLE_OBJECTIVE_MAX_CHARS_DEFAULT)
112
+ try:
113
+ v = int(raw)
114
+ return v if v >= 1 else SOVEREIGN_ROLE_OBJECTIVE_MAX_CHARS_DEFAULT
115
+ except (ValueError, TypeError):
116
+ return SOVEREIGN_ROLE_OBJECTIVE_MAX_CHARS_DEFAULT
117
+
118
+
119
+ def get_review_max_per_phase(scratchpad: Dict[str, Any]) -> int:
120
+ raw = scratchpad.get(SOVEREIGN_ROLE_REVIEW_MAX_PER_PHASE_KEY, SOVEREIGN_ROLE_REVIEW_MAX_PER_PHASE_DEFAULT)
121
+ try:
122
+ v = int(raw)
123
+ return v if v >= 0 else SOVEREIGN_ROLE_REVIEW_MAX_PER_PHASE_DEFAULT
124
+ except (ValueError, TypeError):
125
+ return SOVEREIGN_ROLE_REVIEW_MAX_PER_PHASE_DEFAULT
126
+
127
+
128
+ def get_review_rework_max(scratchpad: Dict[str, Any]) -> int:
129
+ raw = scratchpad.get(SOVEREIGN_ROLE_REVIEW_REWORK_MAX_KEY, SOVEREIGN_ROLE_REVIEW_REWORK_MAX_DEFAULT)
130
+ try:
131
+ v = int(raw)
132
+ return v if v >= 0 else SOVEREIGN_ROLE_REVIEW_REWORK_MAX_DEFAULT
133
+ except (ValueError, TypeError):
134
+ return SOVEREIGN_ROLE_REVIEW_REWORK_MAX_DEFAULT
135
+
136
+
137
+ def scan_secret_literals(text: str) -> List[str]:
138
+ hits: List[str] = []
139
+ for pat in SECRET_PATTERNS:
140
+ m = pat.search(text)
141
+ if m:
142
+ hits.append(m.group(0))
143
+ return hits
144
+
145
+
146
+ def _parse_yaml_minimal(text: str) -> Dict[str, Any]:
147
+ """Minimal YAML-subset parser for the sovereign role manifest.
148
+
149
+ Supports the v1 schema: top-level scalars, sequences of mappings
150
+ (roles[], review_obligations[]), simple inline lists, and nested
151
+ scalar mappings (cross_model_policy, escalation_rules, allowed_self_overrides).
152
+ """
153
+ import re as _re
154
+ result: Dict[str, Any] = {}
155
+ lines = text.splitlines()
156
+ i = 0
157
+ while i < len(lines):
158
+ line = lines[i]
159
+ stripped = line.rstrip()
160
+ if not stripped or stripped.lstrip().startswith("#"):
161
+ i += 1
162
+ continue
163
+ if not line.startswith(" ") and not line.startswith("\t"):
164
+ m = _re.match(r"^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)", stripped)
165
+ if m:
166
+ key = m.group(1)
167
+ val = m.group(2).strip()
168
+ if val.startswith("[") and val.endswith("]"):
169
+ inner = val[1:-1]
170
+ result[key] = [x.strip().strip('"').strip("'") for x in inner.split(",") if x.strip()]
171
+ elif val:
172
+ result[key] = val.strip('"').strip("'")
173
+ else:
174
+ seq, next_i = _parse_block(lines, i + 1)
175
+ result[key] = seq
176
+ i = next_i
177
+ continue
178
+ i += 1
179
+ return result
180
+
181
+
182
+ def _parse_block(lines: List[str], start: int) -> Tuple[Any, int]:
183
+ """Parse a YAML block starting after a key: line."""
184
+ i = start
185
+ if i >= len(lines):
186
+ return None, i
187
+ first = lines[i]
188
+ indent = len(first) - len(first.lstrip())
189
+ if first.strip().startswith("- "):
190
+ items: List[Any] = []
191
+ while i < len(lines):
192
+ line = lines[i]
193
+ if not line.strip() or line.strip().startswith("#"):
194
+ i += 1
195
+ continue
196
+ cur_indent = len(line) - len(line.lstrip())
197
+ if cur_indent < indent:
198
+ break
199
+ if cur_indent == indent and line.strip().startswith("- "):
200
+ item, i = _parse_mapping(lines, i, indent)
201
+ items.append(item)
202
+ else:
203
+ break
204
+ return items, i
205
+ else:
206
+ mapping: Dict[str, Any] = {}
207
+ while i < len(lines):
208
+ line = lines[i]
209
+ if not line.strip() or line.strip().startswith("#"):
210
+ i += 1
211
+ continue
212
+ cur_indent = len(line) - len(line.lstrip())
213
+ if cur_indent < indent:
214
+ break
215
+ m = re.match(r"^\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)", line)
216
+ if m:
217
+ key = m.group(1)
218
+ val = m.group(2).strip()
219
+ if val.startswith("[") and val.endswith("]"):
220
+ inner = val[1:-1]
221
+ mapping[key] = [x.strip().strip('"').strip("'") for x in inner.split(",") if x.strip()]
222
+ elif val:
223
+ mapping[key] = val.strip('"').strip("'")
224
+ else:
225
+ mapping[key] = None
226
+ i += 1
227
+ else:
228
+ i += 1
229
+ return mapping, i
230
+
231
+
232
+ def _parse_mapping(lines: List[str], start: int, base_indent: int) -> Tuple[Dict[str, Any], int]:
233
+ """Parse a single mapping item (- key: val ...) inside a sequence."""
234
+ result: Dict[str, Any] = {}
235
+ first = lines[start]
236
+ content = first.strip()[2:].strip()
237
+ m = re.match(r"([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)", content)
238
+ if m:
239
+ result[m.group(1)] = _coerce(m.group(2).strip())
240
+ i = start + 1
241
+ child_indent = base_indent + 2
242
+ while i < len(lines):
243
+ line = lines[i]
244
+ if not line.strip() or line.strip().startswith("#"):
245
+ i += 1
246
+ continue
247
+ cur_indent = len(line) - len(line.lstrip())
248
+ if cur_indent < child_indent:
249
+ break
250
+ m2 = re.match(r"^\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)", line)
251
+ if m2:
252
+ key = m2.group(1)
253
+ val = m2.group(2).strip()
254
+ if val.startswith("[") and val.endswith("]"):
255
+ inner = val[1:-1]
256
+ result[key] = [x.strip().strip('"').strip("'") for x in inner.split(",") if x.strip()]
257
+ elif val:
258
+ result[key] = _coerce(val)
259
+ else:
260
+ result[key] = None
261
+ i += 1
262
+ return result, i
263
+
264
+
265
+ def _coerce(val: str) -> Any:
266
+ v = val.strip().strip('"').strip("'")
267
+ if v.lower() == "true":
268
+ return True
269
+ if v.lower() == "false":
270
+ return False
271
+ try:
272
+ return int(v)
273
+ except ValueError:
274
+ pass
275
+ return v
276
+
277
+
278
+ def load_manifest(repo_root: Path, scratchpad: Optional[Dict[str, Any]] = None) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
279
+ """Load and return the parsed manifest, or (None, reason_code) when disabled/missing."""
280
+ if scratchpad is not None and not is_role_manifest_enabled(scratchpad):
281
+ return None, ReasonCode.DISABLED.value
282
+ manifest_path = repo_root / MANIFEST_REL
283
+ if not manifest_path.is_file():
284
+ return None, ReasonCode.SCHEMA_INVALID.value
285
+ try:
286
+ text = manifest_path.read_text(encoding="utf-8")
287
+ except Exception:
288
+ return None, ReasonCode.SCHEMA_INVALID.value
289
+ parsed = _parse_yaml_minimal(text)
290
+ return parsed, None
291
+
292
+
293
+ def validate_manifest(parsed: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
294
+ """Validate parsed manifest against v1 schema."""
295
+ if parsed.get("schema_version") not in (1, "1"):
296
+ return False, f"schema_version must be 1, got {parsed.get('schema_version')}"
297
+ roles = parsed.get("roles")
298
+ if not isinstance(roles, list) or not roles:
299
+ return False, "roles[] missing or empty"
300
+ seen_roles = set()
301
+ for r in roles:
302
+ if not isinstance(r, dict):
303
+ return False, "roles[] entry not a mapping"
304
+ rid = r.get("role_id")
305
+ if rid not in VALID_ROLE_IDS:
306
+ return False, f"unknown role_id: {rid}"
307
+ if rid in seen_roles:
308
+ return False, f"duplicate role_id: {rid}"
309
+ seen_roles.add(rid)
310
+ obj = r.get("objective_function", "")
311
+ if not obj or len(str(obj)) == 0:
312
+ return False, f"empty objective_function for role {rid}"
313
+ if len(str(obj)) > OBJECTIVE_FILE_MAX_CHARS:
314
+ return False, f"objective_function > {OBJECTIVE_FILE_MAX_CHARS} chars for role {rid}"
315
+ secrets = scan_secret_literals(str(obj))
316
+ if secrets:
317
+ return False, f"secret-shaped literal in objective_function for role {rid}: {secrets[0]}"
318
+ obligations = parsed.get("review_obligations", [])
319
+ if not isinstance(obligations, list):
320
+ return False, "review_obligations must be a list"
321
+ seen_obl_ids = set()
322
+ for obl in obligations:
323
+ if not isinstance(obl, dict):
324
+ return False, "review_obligations[] entry not a mapping"
325
+ oid = obl.get("obligation_id")
326
+ if not oid:
327
+ return False, "missing obligation_id"
328
+ if oid in seen_obl_ids:
329
+ return False, f"duplicate obligation_id: {oid}"
330
+ seen_obl_ids.add(oid)
331
+ rr = obl.get("reviewer_role")
332
+ tr = obl.get("target_role")
333
+ if rr not in VALID_ROLE_IDS:
334
+ return False, f"unknown reviewer_role: {rr}"
335
+ if tr not in VALID_ROLE_IDS:
336
+ return False, f"unknown target_role: {tr}"
337
+ tp = obl.get("trigger_phase")
338
+ if tp not in VALID_TRIGGER_PHASES:
339
+ return False, f"unknown trigger_phase: {tp}"
340
+ rf = obl.get("review_focus")
341
+ if rf not in VALID_REVIEW_FOCI:
342
+ return False, f"unknown review_focus: {rf}"
343
+ cmp = parsed.get("cross_model_policy")
344
+ if cmp and isinstance(cmp, dict):
345
+ do = cmp.get("default_order")
346
+ if do and do not in VALID_DEFAULT_ORDERS:
347
+ return False, f"unknown cross_model_policy.default_order: {do}"
348
+ return True, None
349
+
350
+
351
+ @dataclass
352
+ class RoleObligation:
353
+ obligation_id: str
354
+ reviewer_role: str
355
+ target_role: str
356
+ trigger_phase: str
357
+ review_focus: str
358
+ artifact_refs: List[str] = field(default_factory=list)
359
+ blocking: bool = False
360
+
361
+
362
+ def list_obligations_for_phase(
363
+ phase_id: str,
364
+ target_role: str,
365
+ manifest: Dict[str, Any],
366
+ max_per_phase: Optional[int] = None,
367
+ ) -> List[RoleObligation]:
368
+ if max_per_phase is None:
369
+ max_per_phase = SOVEREIGN_ROLE_REVIEW_MAX_PER_PHASE_DEFAULT
370
+ obligations = manifest.get("review_obligations", [])
371
+ result: List[RoleObligation] = []
372
+ for obl in obligations:
373
+ if not isinstance(obl, dict):
374
+ continue
375
+ if obl.get("trigger_phase") == phase_id and obl.get("target_role") == target_role:
376
+ result.append(RoleObligation(
377
+ obligation_id=str(obl.get("obligation_id", "")),
378
+ reviewer_role=str(obl.get("reviewer_role", "")),
379
+ target_role=str(obl.get("target_role", "")),
380
+ trigger_phase=str(obl.get("trigger_phase", "")),
381
+ review_focus=str(obl.get("review_focus", "")),
382
+ artifact_refs=list(obl.get("artifact_refs", [])) if isinstance(obl.get("artifact_refs"), list) else [],
383
+ blocking=bool(obl.get("blocking", False)),
384
+ ))
385
+ if len(result) >= max_per_phase:
386
+ break
387
+ return result
388
+
389
+
390
+ def resolve_role_objective(role_id: str, manifest: Dict[str, Any]) -> Optional[str]:
391
+ roles = manifest.get("roles", [])
392
+ for r in roles:
393
+ if isinstance(r, dict) and r.get("role_id") == role_id:
394
+ return str(r.get("objective_function", ""))
395
+ return None
396
+
397
+
398
+ def build_objective_injection_block(
399
+ scratchpad: Dict[str, Any],
400
+ role_id: str,
401
+ repo_root: Optional[Path] = None,
402
+ ) -> Tuple[Optional[str], Optional[str]]:
403
+ if not is_role_manifest_enabled(scratchpad):
404
+ return None, ReasonCode.DISABLED.value
405
+ if repo_root is None:
406
+ repo_root = Path.cwd()
407
+ manifest, err = load_manifest(repo_root, scratchpad)
408
+ if manifest is None:
409
+ return None, err
410
+ objective = resolve_role_objective(role_id, manifest)
411
+ if objective is None:
412
+ return None, ReasonCode.UNKNOWN_ROLE.value
413
+ max_chars = get_objective_max_chars(scratchpad)
414
+ truncated = objective[:max_chars]
415
+ block = f"## Role objective ({role_id})\n\n{truncated}"
416
+ return block, None
417
+
418
+
419
+ def resolve_critic_ordering(
420
+ manifest: Dict[str, Any],
421
+ obligation_id: Optional[str] = None,
422
+ ) -> str:
423
+ cmp_section = manifest.get("cross_model_policy", {})
424
+ if not isinstance(cmp_section, dict):
425
+ return "role_review_first"
426
+ default_order = str(cmp_section.get("default_order", "role_review_first"))
427
+ if obligation_id:
428
+ overrides = cmp_section.get("per_obligation_overrides", {})
429
+ if isinstance(overrides, dict) and obligation_id in overrides:
430
+ return str(overrides[obligation_id])
431
+ if default_order in VALID_DEFAULT_ORDERS:
432
+ return default_order
433
+ return "role_review_first"
434
+
435
+
436
+ def build_review_row(
437
+ obligation: RoleObligation,
438
+ producer_evidence_ref: str,
439
+ orchestrator_run_id: str,
440
+ verdict: str = "pending",
441
+ findings_ref: str = "",
442
+ ) -> Dict[str, Any]:
443
+ return {
444
+ "schema_version": SCHEMA_VERSION,
445
+ "obligation_id": obligation.obligation_id,
446
+ "reviewer_role": obligation.reviewer_role,
447
+ "target_role": obligation.target_role,
448
+ "trigger_phase": obligation.trigger_phase,
449
+ "review_focus": obligation.review_focus,
450
+ "producer_evidence_ref": producer_evidence_ref,
451
+ "orchestrator_run_id": orchestrator_run_id,
452
+ "ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
453
+ "verdict": verdict,
454
+ "blocking": obligation.blocking,
455
+ "findings_ref": findings_ref,
456
+ }
457
+
458
+
459
+ def append_review_row(repo_root: Path, row: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
460
+ jsonl_path = repo_root / REVIEWS_JSONL_REL
461
+ jsonl_path.parent.mkdir(parents=True, exist_ok=True)
462
+ try:
463
+ with open(jsonl_path, "a", encoding="utf-8") as f:
464
+ f.write(json.dumps(row, ensure_ascii=False) + "\n")
465
+ return True, None
466
+ except Exception as exc:
467
+ return False, f"{ReasonCode.REVIEW_DISPATCH_FAILED.value}: {exc}"
468
+
469
+
470
+ @dataclass
471
+ class RoleReviewDispatch:
472
+ trigger_phase: str
473
+ producer_role: str
474
+ reviewer_role: str
475
+ obligation_id: str
476
+ boundary_token: str
477
+ artifact_refs: List[str]
478
+ producer_evidence_ref: str
479
+ spawn_only: bool = True
480
+
481
+
482
+ def dispatch_role_review(
483
+ obligation: RoleObligation,
484
+ producer_evidence_ref: str,
485
+ trigger_phase: str,
486
+ producer_role: str,
487
+ scratchpad: Optional[Dict[str, Any]] = None,
488
+ ) -> Tuple[Optional[RoleReviewDispatch], str]:
489
+ """Build a spawn-only review dispatch descriptor (BUG-0006).
490
+
491
+ Returns (dispatch, reason_code). The dispatch describes the supplementary
492
+ fresh-reviewer subagent to spawn — the orchestrator spawns, never
493
+ substitutes the producer phase role.
494
+ """
495
+ if scratchpad is not None and not is_role_manifest_enabled(scratchpad):
496
+ return None, ReasonCode.DISABLED.value
497
+ dispatch = RoleReviewDispatch(
498
+ trigger_phase=trigger_phase,
499
+ producer_role=producer_role,
500
+ reviewer_role=obligation.reviewer_role,
501
+ obligation_id=obligation.obligation_id,
502
+ boundary_token="role_review",
503
+ artifact_refs=list(obligation.artifact_refs),
504
+ producer_evidence_ref=producer_evidence_ref,
505
+ spawn_only=True,
506
+ )
507
+ return dispatch, ""
508
+
509
+
510
+ def self_test() -> bool:
511
+ ok, _ = validate_manifest({
512
+ "schema_version": 1,
513
+ "roles": [
514
+ {"role_id": "po", "objective_function": "Maximize user value delivery"},
515
+ ],
516
+ "review_obligations": [
517
+ {
518
+ "obligation_id": "O1",
519
+ "reviewer_role": "po",
520
+ "target_role": "tech-lead",
521
+ "trigger_phase": "architecture",
522
+ "review_focus": "user_value_drift",
523
+ "artifact_refs": ["docs/engineering/architecture.md"],
524
+ "blocking": False,
525
+ },
526
+ ],
527
+ "allowed_self_overrides": ["verbosity", "detail_level", "tone"],
528
+ "cross_model_policy": {"default_order": "role_review_first"},
529
+ "escalation_rules": {"rework_max": 1},
530
+ })
531
+ return ok
532
+
533
+
534
+ def main() -> int:
535
+ import argparse
536
+ p = argparse.ArgumentParser(description=__doc__)
537
+ p.add_argument("--self-test", action="store_true")
538
+ args = p.parse_args()
539
+ if args.self_test:
540
+ ok = self_test()
541
+ print("[SOVEREIGN_ROLE_MANIFEST_SELF_TEST_OK]" if ok else "[SOVEREIGN_ROLE_MANIFEST_SELF_TEST_FAIL]")
542
+ return 0 if ok else 1
543
+ return 0
544
+
545
+
546
+ if __name__ == "__main__":
547
+ sys.exit(main())