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,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())
|