@trac3er/oh-my-god 2.0.8 → 2.0.9

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 (118) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.gemini/settings.json +11 -0
  4. package/.kimi/mcp.json +11 -0
  5. package/CHANGELOG.md +10 -0
  6. package/OMG-setup.sh +1 -1
  7. package/OMG_COMPAT_CONTRACT.md +10 -4
  8. package/README.md +1 -0
  9. package/build/lib/commands/OMG:forge.md +92 -0
  10. package/build/lib/commands/OMG:mode.md +13 -13
  11. package/build/lib/commands/OMG:session-branch.md +17 -1
  12. package/build/lib/commands/OMG:session-fork.md +5 -1
  13. package/build/lib/commands/OMG:session-merge.md +5 -1
  14. package/build/lib/control_plane/server.py +4 -0
  15. package/build/lib/control_plane/service.py +55 -0
  16. package/build/lib/hooks/setup_wizard.py +21 -1
  17. package/build/lib/hooks/shadow_manager.py +25 -2
  18. package/build/lib/hooks/state_migration.py +3 -0
  19. package/build/lib/plugins/dephealth/cve_scanner.py +91 -0
  20. package/build/lib/plugins/dephealth/vuln_analyzer.py +7 -0
  21. package/build/lib/registry/omg-capability.schema.json +83 -1
  22. package/build/lib/runtime/adoption.py +12 -4
  23. package/build/lib/runtime/artifact_parsers.py +161 -0
  24. package/build/lib/runtime/background_verification.py +48 -0
  25. package/build/lib/runtime/claim_judge.py +184 -7
  26. package/build/lib/runtime/contract_compiler.py +118 -9
  27. package/build/lib/runtime/evidence_query.py +203 -0
  28. package/build/lib/runtime/omg_mcp_server.py +19 -0
  29. package/build/lib/runtime/playwright_adapter.py +39 -0
  30. package/build/lib/runtime/proof_chain.py +136 -8
  31. package/build/lib/runtime/proof_gate.py +102 -0
  32. package/build/lib/runtime/providers/gemini_provider.py +7 -0
  33. package/build/lib/runtime/providers/kimi_provider.py +7 -0
  34. package/build/lib/runtime/repro_pack.py +292 -0
  35. package/build/lib/runtime/runtime_profile.py +87 -15
  36. package/build/lib/runtime/security_check.py +86 -3
  37. package/build/lib/runtime/test_intent_lock.py +47 -0
  38. package/build/lib/runtime/tracebank.py +33 -3
  39. package/build/lib/runtime/verification_loop.py +73 -0
  40. package/commands/OMG:forge.md +92 -0
  41. package/commands/OMG:mode.md +13 -13
  42. package/commands/OMG:session-branch.md +17 -1
  43. package/commands/OMG:session-fork.md +5 -1
  44. package/commands/OMG:session-merge.md +5 -1
  45. package/control_plane/server.py +4 -0
  46. package/control_plane/service.py +55 -0
  47. package/dist/enterprise/bundle/.gemini/settings.json +11 -0
  48. package/dist/enterprise/bundle/.kimi/mcp.json +11 -0
  49. package/dist/enterprise/bundle/OMG_COMPAT_CONTRACT.md +9 -3
  50. package/dist/enterprise/bundle/registry/omg-capability.schema.json +83 -1
  51. package/dist/enterprise/bundle/settings.json +1 -0
  52. package/dist/enterprise/manifest.json +17 -3
  53. package/dist/public/bundle/.agents/skills/omg/incident-replay/SKILL.md +1 -1
  54. package/dist/public/bundle/.agents/skills/omg/incident-replay/openai.yaml +1 -1
  55. package/dist/public/bundle/.agents/skills/omg/lsp-pack/SKILL.md +1 -1
  56. package/dist/public/bundle/.agents/skills/omg/lsp-pack/openai.yaml +1 -1
  57. package/dist/public/bundle/.agents/skills/omg/mcp-fabric/SKILL.md +1 -1
  58. package/dist/public/bundle/.agents/skills/omg/mcp-fabric/openai.yaml +1 -1
  59. package/dist/public/bundle/.agents/skills/omg/plan-council/SKILL.md +1 -1
  60. package/dist/public/bundle/.agents/skills/omg/plan-council/openai.yaml +1 -1
  61. package/dist/public/bundle/.agents/skills/omg/preflight/SKILL.md +1 -1
  62. package/dist/public/bundle/.agents/skills/omg/preflight/openai.yaml +1 -1
  63. package/dist/public/bundle/.agents/skills/omg/proof-gate/SKILL.md +1 -1
  64. package/dist/public/bundle/.agents/skills/omg/proof-gate/openai.yaml +1 -1
  65. package/dist/public/bundle/.agents/skills/omg/remote-supervisor/SKILL.md +1 -1
  66. package/dist/public/bundle/.agents/skills/omg/remote-supervisor/openai.yaml +1 -1
  67. package/dist/public/bundle/.agents/skills/omg/robotics/SKILL.md +1 -1
  68. package/dist/public/bundle/.agents/skills/omg/robotics/openai.yaml +1 -1
  69. package/dist/public/bundle/.agents/skills/omg/secure-worktree-pipeline/SKILL.md +1 -1
  70. package/dist/public/bundle/.agents/skills/omg/secure-worktree-pipeline/openai.yaml +1 -1
  71. package/dist/public/bundle/.agents/skills/omg/security-check/SKILL.md +1 -1
  72. package/dist/public/bundle/.agents/skills/omg/security-check/openai.yaml +1 -1
  73. package/dist/public/bundle/.agents/skills/omg/test-intent-lock/SKILL.md +1 -1
  74. package/dist/public/bundle/.agents/skills/omg/test-intent-lock/openai.yaml +1 -1
  75. package/dist/public/bundle/.agents/skills/omg/tracebank/SKILL.md +1 -1
  76. package/dist/public/bundle/.agents/skills/omg/tracebank/openai.yaml +1 -1
  77. package/dist/public/bundle/.agents/skills/omg/vision/SKILL.md +1 -1
  78. package/dist/public/bundle/.agents/skills/omg/vision/openai.yaml +1 -1
  79. package/dist/public/bundle/.gemini/settings.json +11 -0
  80. package/dist/public/bundle/.kimi/mcp.json +11 -0
  81. package/dist/public/bundle/OMG_COMPAT_CONTRACT.md +9 -3
  82. package/dist/public/bundle/registry/omg-capability.schema.json +83 -1
  83. package/dist/public/bundle/settings.json +2 -1
  84. package/dist/public/manifest.json +43 -29
  85. package/docs/proof.md +1 -0
  86. package/hooks/setup_wizard.py +21 -1
  87. package/hooks/shadow_manager.py +25 -2
  88. package/hooks/state_migration.py +3 -0
  89. package/hud/omg-hud.mjs +66 -3
  90. package/package.json +1 -1
  91. package/plugins/advanced/plugin.json +1 -1
  92. package/plugins/core/plugin.json +1 -1
  93. package/plugins/dephealth/cve_scanner.py +91 -0
  94. package/plugins/dephealth/vuln_analyzer.py +7 -0
  95. package/pyproject.toml +1 -1
  96. package/registry/omg-capability.schema.json +83 -1
  97. package/runtime/adoption.py +13 -5
  98. package/runtime/artifact_parsers.py +161 -0
  99. package/runtime/background_verification.py +48 -0
  100. package/runtime/claim_judge.py +184 -7
  101. package/runtime/contract_compiler.py +118 -9
  102. package/runtime/evidence_query.py +203 -0
  103. package/runtime/omg_mcp_server.py +19 -0
  104. package/runtime/playwright_adapter.py +39 -0
  105. package/runtime/proof_chain.py +136 -8
  106. package/runtime/proof_gate.py +102 -0
  107. package/runtime/providers/gemini_provider.py +7 -0
  108. package/runtime/providers/kimi_provider.py +7 -0
  109. package/runtime/repro_pack.py +292 -0
  110. package/runtime/runtime_profile.py +87 -15
  111. package/runtime/security_check.py +86 -3
  112. package/runtime/test_intent_lock.py +47 -0
  113. package/runtime/tracebank.py +33 -3
  114. package/runtime/verification_loop.py +73 -0
  115. package/scripts/omg.py +30 -3
  116. package/settings.json +4 -3
  117. package/tools/python_sandbox.py +9 -6
  118. package/tools/session_snapshot.py +146 -40
@@ -10,7 +10,11 @@ import urllib.request
10
10
 
11
11
 
12
12
  OSV_BATCH_URL = "https://api.osv.dev/v1/querybatch"
13
+ KEV_URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"
14
+ EPSS_URL_TEMPLATE = "https://api.first.org/data/v1/epss?cve={cve_id}"
13
15
  CACHE_REL_PATH = Path(".omg") / "state" / "cve-cache.json"
16
+ KEV_CACHE_REL_PATH = Path(".omg") / "state" / "kev-cache.json"
17
+ EPSS_CACHE_REL_PATH = Path(".omg") / "state" / "epss-cache.json"
14
18
  CACHE_TTL_HOURS = 24
15
19
 
16
20
 
@@ -72,6 +76,93 @@ def scan_for_cves(dependency_list: list[dict[str, str]], project_dir: str = ".")
72
76
  return scan_result
73
77
 
74
78
 
79
+ def enrich_with_kev(finding: dict[str, Any], cache_dir: str) -> dict[str, Any]:
80
+ result = dict(finding)
81
+ if not _dep_health_enabled():
82
+ result["kev_listed"] = False
83
+ return result
84
+
85
+ cve_id = str(finding.get("id", "")).strip()
86
+ if not cve_id:
87
+ result["kev_listed"] = False
88
+ return result
89
+
90
+ cache_path = Path(cache_dir) / KEV_CACHE_REL_PATH
91
+ cached = _load_cache(cache_path)
92
+
93
+ if cached and _is_cache_fresh(cached.get("fetched_at")):
94
+ result["kev_listed"] = cve_id in cached.get("cve_ids", [])
95
+ return result
96
+
97
+ try:
98
+ kev_data = _fetch_kev_catalog()
99
+ kev_ids = [v.get("cveID", "") for v in kev_data.get("vulnerabilities", [])]
100
+ _save_cache(cache_path, {
101
+ "fetched_at": datetime.now(timezone.utc).isoformat(),
102
+ "cve_ids": kev_ids,
103
+ })
104
+ result["kev_listed"] = cve_id in kev_ids
105
+ except (urllib.error.URLError, OSError, json.JSONDecodeError):
106
+ if cached:
107
+ result["kev_listed"] = cve_id in cached.get("cve_ids", [])
108
+ else:
109
+ result["kev_listed"] = False
110
+ return result
111
+
112
+
113
+ def enrich_with_epss(finding: dict[str, Any], cache_dir: str) -> dict[str, Any]:
114
+ result = dict(finding)
115
+ if not _dep_health_enabled():
116
+ result["epss_score"] = None
117
+ return result
118
+
119
+ cve_id = str(finding.get("id", "")).strip()
120
+ if not cve_id:
121
+ result["epss_score"] = None
122
+ return result
123
+
124
+ cache_path = Path(cache_dir) / EPSS_CACHE_REL_PATH
125
+ cached = _load_cache(cache_path) or {}
126
+ entries = cached.get("entries", {})
127
+
128
+ entry = entries.get(cve_id)
129
+ if entry and _is_cache_fresh(entry.get("fetched_at")):
130
+ result["epss_score"] = entry.get("epss")
131
+ return result
132
+
133
+ try:
134
+ epss_data = _fetch_epss_score(cve_id)
135
+ data_list = epss_data.get("data", [])
136
+ score = float(data_list[0].get("epss", 0)) if data_list else None
137
+ entries[cve_id] = {
138
+ "epss": score,
139
+ "fetched_at": datetime.now(timezone.utc).isoformat(),
140
+ }
141
+ _save_cache(cache_path, {"entries": entries})
142
+ result["epss_score"] = score
143
+ except (urllib.error.URLError, OSError, json.JSONDecodeError, ValueError, TypeError):
144
+ if entry:
145
+ result["epss_score"] = entry.get("epss")
146
+ else:
147
+ result["epss_score"] = None
148
+ return result
149
+
150
+
151
+ def _fetch_kev_catalog() -> dict[str, Any]:
152
+ request = urllib.request.Request(KEV_URL, headers={"Accept": "application/json"})
153
+ with urllib.request.urlopen(request, timeout=15) as response:
154
+ body = response.read().decode("utf-8")
155
+ return json.loads(body)
156
+
157
+
158
+ def _fetch_epss_score(cve_id: str) -> dict[str, Any]:
159
+ url = EPSS_URL_TEMPLATE.format(cve_id=cve_id)
160
+ request = urllib.request.Request(url, headers={"Accept": "application/json"})
161
+ with urllib.request.urlopen(request, timeout=15) as response:
162
+ body = response.read().decode("utf-8")
163
+ return json.loads(body)
164
+
165
+
75
166
  def _query_osv_batch(dependency_list: list[dict[str, str]]) -> dict[str, Any]:
76
167
  payload = {
77
168
  "queries": [
@@ -3,6 +3,8 @@ import re
3
3
  from pathlib import Path
4
4
  from typing import Any
5
5
 
6
+ from plugins.dephealth.cve_scanner import enrich_with_kev, enrich_with_epss
7
+
6
8
 
7
9
  _CRITICAL_PATTERN = re.compile(r"\bcritical\b", re.IGNORECASE)
8
10
  _HIGH_PATTERN = re.compile(r"\bhigh\b", re.IGNORECASE)
@@ -33,6 +35,9 @@ def analyze_reachability(cve_result: dict[str, Any], project_dir: str) -> dict[s
33
35
  risk_level = _classify_risk(reachability, summary)
34
36
  recommendation = _build_recommendation(package, fixed_version, reachability)
35
37
 
38
+ kev_enriched = enrich_with_kev({"id": cve_id}, project_dir)
39
+ epss_enriched = enrich_with_epss({"id": cve_id}, project_dir)
40
+
36
41
  return {
37
42
  "package": package,
38
43
  "cve_id": cve_id,
@@ -40,6 +45,8 @@ def analyze_reachability(cve_result: dict[str, Any], project_dir: str) -> dict[s
40
45
  "import_locations": sorted(imported_files),
41
46
  "risk_level": risk_level,
42
47
  "recommendation": recommendation,
48
+ "kev_listed": kev_enriched.get("kev_listed", False),
49
+ "epss_score": epss_enriched.get("epss_score"),
43
50
  }
44
51
 
45
52
 
@@ -43,7 +43,9 @@
43
43
  "type": "string",
44
44
  "enum": [
45
45
  "claude",
46
- "codex"
46
+ "codex",
47
+ "gemini",
48
+ "kimi"
47
49
  ]
48
50
  },
49
51
  "minItems": 1
@@ -281,6 +283,86 @@
281
283
  }
282
284
  },
283
285
  "additionalProperties": true
286
+ },
287
+ "gemini": {
288
+ "type": "object",
289
+ "required": [
290
+ "compilation_targets",
291
+ "mcp",
292
+ "skills",
293
+ "automations"
294
+ ],
295
+ "properties": {
296
+ "compilation_targets": {
297
+ "type": "array",
298
+ "minItems": 1,
299
+ "items": {
300
+ "type": "string"
301
+ }
302
+ },
303
+ "mcp": {
304
+ "type": "array",
305
+ "minItems": 1,
306
+ "items": {
307
+ "type": "string"
308
+ }
309
+ },
310
+ "skills": {
311
+ "type": "array",
312
+ "minItems": 1,
313
+ "items": {
314
+ "type": "string"
315
+ }
316
+ },
317
+ "automations": {
318
+ "type": "array",
319
+ "minItems": 1,
320
+ "items": {
321
+ "type": "string"
322
+ }
323
+ }
324
+ },
325
+ "additionalProperties": true
326
+ },
327
+ "kimi": {
328
+ "type": "object",
329
+ "required": [
330
+ "compilation_targets",
331
+ "mcp",
332
+ "skills",
333
+ "automations"
334
+ ],
335
+ "properties": {
336
+ "compilation_targets": {
337
+ "type": "array",
338
+ "minItems": 1,
339
+ "items": {
340
+ "type": "string"
341
+ }
342
+ },
343
+ "mcp": {
344
+ "type": "array",
345
+ "minItems": 1,
346
+ "items": {
347
+ "type": "string"
348
+ }
349
+ },
350
+ "skills": {
351
+ "type": "array",
352
+ "minItems": 1,
353
+ "items": {
354
+ "type": "string"
355
+ }
356
+ },
357
+ "automations": {
358
+ "type": "array",
359
+ "minItems": 1,
360
+ "items": {
361
+ "type": "string"
362
+ }
363
+ }
364
+ },
365
+ "additionalProperties": true
284
366
  }
285
367
  },
286
368
  "additionalProperties": true
@@ -2,8 +2,8 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import json
5
+ import importlib
5
6
  from pathlib import Path
6
- from typing import Any
7
7
 
8
8
 
9
9
  CANONICAL_BRAND = "OMG"
@@ -15,6 +15,8 @@ CANONICAL_VERSION = "2.0.8"
15
15
 
16
16
  VALID_ADOPTION_MODES = ("omg-only", "coexist")
17
17
  VALID_PRESETS = ("safe", "balanced", "interop", "labs")
18
+ CANONICAL_MODE_NAMES = ("chill", "focused", "exploratory")
19
+ """Cross-surface user-facing mode names reserved for the shared mode system."""
18
20
 
19
21
  MANAGED_PRESET_FLAGS = (
20
22
  "SETUP",
@@ -91,6 +93,12 @@ def get_preset_features(preset: str | None) -> dict[str, bool]:
91
93
  return dict(PRESET_FEATURES[resolved])
92
94
 
93
95
 
96
+ def get_mode_profile(mode: str) -> dict[str, object]:
97
+ runtime_profile = importlib.import_module("runtime.runtime_profile")
98
+ loader = getattr(runtime_profile, "load_canonical_mode_profile")
99
+ return loader(mode)
100
+
101
+
94
102
  def _resolve_claude_dir(base_dir: Path) -> Path:
95
103
  nested = base_dir / ".claude"
96
104
  if nested.exists():
@@ -180,7 +188,7 @@ def build_adoption_report(
180
188
  requested_mode: str | None = None,
181
189
  preset: str | None = None,
182
190
  adopt: str = "auto",
183
- ) -> dict[str, Any]:
191
+ ) -> dict[str, object]:
184
192
  detected_ecosystems = detect_ecosystems(project_dir) if adopt == "auto" else []
185
193
  recommended_mode = recommend_mode(detected_ecosystems)
186
194
  selected_mode = requested_mode if requested_mode in VALID_ADOPTION_MODES else recommended_mode
@@ -204,9 +212,9 @@ def build_adoption_report(
204
212
  }
205
213
 
206
214
 
207
- def write_adoption_report(project_dir: str | Path, report: dict[str, Any]) -> str:
215
+ def write_adoption_report(project_dir: str | Path, report: dict[str, object]) -> str:
208
216
  state_dir = Path(project_dir) / ".omg" / "state"
209
217
  state_dir.mkdir(parents=True, exist_ok=True)
210
218
  report_path = state_dir / "adoption-report.json"
211
- report_path.write_text(json.dumps(report, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
219
+ _ = report_path.write_text(json.dumps(report, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
212
220
  return str(report_path)
@@ -0,0 +1,161 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any
6
+ from xml.etree import ElementTree
7
+
8
+
9
+ def parse_junit(path: str) -> dict[str, Any]:
10
+ file_path = Path(path)
11
+ if not file_path.exists():
12
+ return {"valid": False, "summary": {}, "error": "file_not_found"}
13
+
14
+ try:
15
+ root = ElementTree.parse(file_path).getroot()
16
+ except (ElementTree.ParseError, OSError, ValueError) as exc:
17
+ return {"valid": False, "summary": {}, "error": f"junit_parse_error:{exc}"}
18
+
19
+ root_name = _local_name(root.tag)
20
+ if root_name not in {"testsuite", "testsuites"}:
21
+ return {
22
+ "valid": False,
23
+ "summary": {"root": root_name},
24
+ "error": "junit_invalid_root",
25
+ }
26
+
27
+ tests_value = root.attrib.get("tests", "")
28
+ return {
29
+ "valid": True,
30
+ "summary": {"root": root_name, "tests": str(tests_value).strip()},
31
+ "error": None,
32
+ }
33
+
34
+
35
+ def parse_sarif(path: str) -> dict[str, Any]:
36
+ payload, error = _load_json(Path(path))
37
+ if error:
38
+ return {"valid": False, "summary": {}, "error": error}
39
+
40
+ runs = payload.get("runs") if isinstance(payload, dict) else None
41
+ if not isinstance(runs, list):
42
+ return {"valid": False, "summary": {}, "error": "sarif_missing_runs"}
43
+
44
+ return {
45
+ "valid": True,
46
+ "summary": {"runs": len(runs), "version": str(payload.get("version", "")).strip()},
47
+ "error": None,
48
+ }
49
+
50
+
51
+ def parse_coverage(path: str) -> dict[str, Any]:
52
+ file_path = Path(path)
53
+ if not file_path.exists():
54
+ return {"valid": False, "summary": {}, "error": "file_not_found"}
55
+
56
+ xml_result = _parse_coverage_xml(file_path)
57
+ if xml_result["valid"]:
58
+ return xml_result
59
+
60
+ json_result = _parse_coverage_json(file_path)
61
+ if json_result["valid"]:
62
+ return json_result
63
+
64
+ return {
65
+ "valid": False,
66
+ "summary": {},
67
+ "error": xml_result.get("error") or json_result.get("error") or "coverage_missing_keys",
68
+ }
69
+
70
+
71
+ def parse_browser_trace(path: str) -> dict[str, Any]:
72
+ payload, error = _load_json(Path(path))
73
+ if error:
74
+ return {"valid": False, "summary": {}, "error": error}
75
+
76
+ if not isinstance(payload, dict):
77
+ return {"valid": False, "summary": {}, "error": "browser_trace_invalid_payload"}
78
+
79
+ has_trace = "trace" in payload
80
+ has_events = isinstance(payload.get("events"), list)
81
+ if not (has_trace or has_events):
82
+ return {
83
+ "valid": False,
84
+ "summary": {},
85
+ "error": "browser_trace_missing_trace_or_events",
86
+ }
87
+
88
+ return {
89
+ "valid": True,
90
+ "summary": {"has_trace": has_trace, "event_count": len(payload.get("events", [])) if has_events else 0},
91
+ "error": None,
92
+ }
93
+
94
+
95
+ def parse_diff_hunk(path: str) -> dict[str, Any]:
96
+ file_path = Path(path)
97
+ if not file_path.exists():
98
+ return {"valid": False, "summary": {}, "error": "file_not_found"}
99
+
100
+ try:
101
+ content = file_path.read_text(encoding="utf-8")
102
+ except (OSError, UnicodeDecodeError) as exc:
103
+ return {"valid": False, "summary": {}, "error": f"diff_read_error:{exc}"}
104
+
105
+ if "@@" not in content:
106
+ return {"valid": False, "summary": {}, "error": "diff_missing_hunk_marker"}
107
+
108
+ hunk_count = content.count("@@") // 2 if content.count("@@") >= 2 else 1
109
+ return {"valid": True, "summary": {"hunk_count": hunk_count}, "error": None}
110
+
111
+
112
+ def _parse_coverage_xml(file_path: Path) -> dict[str, Any]:
113
+ try:
114
+ root = ElementTree.parse(file_path).getroot()
115
+ except (ElementTree.ParseError, OSError, ValueError):
116
+ return {"valid": False, "summary": {}, "error": "coverage_xml_parse_error"}
117
+
118
+ if "line-rate" in root.attrib:
119
+ return {
120
+ "valid": True,
121
+ "summary": {"line-rate": str(root.attrib.get("line-rate", "")).strip()},
122
+ "error": None,
123
+ }
124
+
125
+ return {"valid": False, "summary": {}, "error": "coverage_missing_line_rate"}
126
+
127
+
128
+ def _parse_coverage_json(file_path: Path) -> dict[str, Any]:
129
+ payload, error = _load_json(file_path)
130
+ if error:
131
+ return {"valid": False, "summary": {}, "error": error}
132
+
133
+ if not isinstance(payload, dict):
134
+ return {"valid": False, "summary": {}, "error": "coverage_json_invalid_payload"}
135
+
136
+ if "coverage" not in payload:
137
+ return {"valid": False, "summary": {}, "error": "coverage_missing_coverage_key"}
138
+
139
+ return {
140
+ "valid": True,
141
+ "summary": {"coverage": payload.get("coverage")},
142
+ "error": None,
143
+ }
144
+
145
+
146
+ def _load_json(file_path: Path) -> tuple[Any, str | None]:
147
+ if not file_path.exists():
148
+ return {}, "file_not_found"
149
+
150
+ try:
151
+ payload = json.loads(file_path.read_text(encoding="utf-8"))
152
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
153
+ return {}, f"json_parse_error:{exc}"
154
+
155
+ return payload, None
156
+
157
+
158
+ def _local_name(tag: str) -> str:
159
+ if "}" not in tag:
160
+ return tag
161
+ return tag.rsplit("}", 1)[-1]
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from datetime import datetime, timezone
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ BACKGROUND_VERIFICATION_REL_PATH = Path(".omg") / "state" / "background-verification.json"
9
+
10
+ _VALID_STATUSES = frozenset({"running", "ok", "error", "blocked"})
11
+
12
+
13
+ def publish_verification_state(
14
+ project_dir: str,
15
+ run_id: str,
16
+ status: str,
17
+ blockers: list[str],
18
+ evidence_links: list[str],
19
+ progress: dict[str, Any],
20
+ ) -> str:
21
+ state = {
22
+ "schema": "BackgroundVerificationState",
23
+ "schema_version": 2,
24
+ "run_id": run_id,
25
+ "status": status if status in _VALID_STATUSES else "error",
26
+ "blockers": blockers,
27
+ "evidence_links": evidence_links,
28
+ "progress": progress,
29
+ "updated_at": datetime.now(timezone.utc).isoformat(),
30
+ }
31
+
32
+ path = Path(project_dir) / BACKGROUND_VERIFICATION_REL_PATH
33
+ path.parent.mkdir(parents=True, exist_ok=True)
34
+ path.write_text(json.dumps(state, indent=2, ensure_ascii=True), encoding="utf-8")
35
+ return str(path)
36
+
37
+
38
+ def read_verification_state(project_dir: str) -> dict[str, Any] | None:
39
+ path = Path(project_dir) / BACKGROUND_VERIFICATION_REL_PATH
40
+ if not path.exists():
41
+ return None
42
+ try:
43
+ payload = json.loads(path.read_text(encoding="utf-8"))
44
+ if isinstance(payload, dict) and payload.get("schema") == "BackgroundVerificationState":
45
+ return payload
46
+ except (json.JSONDecodeError, OSError):
47
+ pass
48
+ return None