@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
@@ -1,15 +1,68 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
4
+ from pathlib import Path
3
5
  from typing import Any
4
6
 
7
+ from runtime import artifact_parsers
8
+ from runtime.evidence_query import get_evidence_pack
9
+
10
+
11
+ def judge_claims(project_dir: str, claims: list[dict[str, Any]]) -> dict[str, Any]:
12
+ root = Path(project_dir)
13
+ evidence_dir = root / ".omg" / "evidence"
14
+ evidence_dir.mkdir(parents=True, exist_ok=True)
15
+
16
+ results: list[dict[str, Any]] = []
17
+ aggregate_tokens: list[str] = []
18
+
19
+ for index, claim in enumerate(claims):
20
+ run_id = str(claim.get("run_id", "")).strip()
21
+ resolved_claim = dict(claim)
22
+
23
+ if run_id:
24
+ evidence_pack = get_evidence_pack(project_dir, run_id)
25
+ trace_ids: list[str] = []
26
+ if isinstance(evidence_pack, dict):
27
+ trace_ids = _as_non_empty_str_list(evidence_pack.get("trace_ids"))
28
+ resolved_claim = {
29
+ **claim,
30
+ "artifacts": [f".omg/evidence/{run_id}.json"],
31
+ "trace_ids": trace_ids,
32
+ }
33
+
34
+ result = judge_claim(resolved_claim)
35
+ result_with_run = {**result, "run_id": run_id}
36
+ results.append(result_with_run)
37
+ aggregate_tokens.append(str(result.get("verdict", "")).strip().lower())
38
+
39
+ artifact_run_id = run_id or f"unknown-{index + 1}"
40
+ artifact_path = evidence_dir / f"claim-judge-{_sanitize_run_id(artifact_run_id)}.json"
41
+ artifact_payload = {
42
+ "schema": "ClaimJudgeResult",
43
+ "run_id": run_id,
44
+ "claim": claim,
45
+ "result": result,
46
+ }
47
+ artifact_path.write_text(json.dumps(artifact_payload, indent=2, sort_keys=True), encoding="utf-8")
48
+
49
+ verdict = "pass"
50
+ if any(token == "fail" for token in aggregate_tokens):
51
+ verdict = "fail"
52
+ elif any(token == "block" for token in aggregate_tokens):
53
+ verdict = "insufficient"
54
+
55
+ return {"schema": "ClaimJudgeResults", "verdict": verdict, "results": results}
56
+
5
57
 
6
58
  def judge_claim(claim: dict[str, Any]) -> dict[str, Any]:
7
- claim_type = str(claim.get("claim_type", "")).strip()
8
- subject = str(claim.get("subject", "")).strip()
9
- artifacts = _as_non_empty_str_list(claim.get("artifacts"))
10
- trace_ids = _as_non_empty_str_list(claim.get("trace_ids"))
11
- security_scans = claim.get("security_scans")
12
- browser_evidence = claim.get("browser_evidence")
59
+ normalized_claim = _normalize_claim(claim)
60
+ claim_type = str(normalized_claim.get("claim_type", "")).strip()
61
+ subject = str(normalized_claim.get("subject", "")).strip()
62
+ artifacts = _as_non_empty_str_list(normalized_claim.get("artifacts"))
63
+ trace_ids = _as_non_empty_str_list(normalized_claim.get("trace_ids"))
64
+ security_scans = normalized_claim.get("security_scans")
65
+ browser_evidence = normalized_claim.get("browser_evidence")
13
66
 
14
67
  reasons: list[dict[str, Any]] = []
15
68
 
@@ -49,6 +102,22 @@ def judge_claim(claim: dict[str, Any]) -> dict[str, Any]:
49
102
  }
50
103
  )
51
104
 
105
+ raw_artifacts = _extract_artifact_dicts(claim)
106
+ project_dir = str(claim.get("project_dir", "."))
107
+ for artifact in raw_artifacts:
108
+ parse_result = parse_artifact_content(artifact=artifact, project_dir=project_dir)
109
+ if parse_result.get("parsed"):
110
+ continue
111
+ kind = str(parse_result.get("kind", "artifact")).strip().lower() or "artifact"
112
+ error = str(parse_result.get("error", "parse_error")).strip() or "parse_error"
113
+ reasons.append(
114
+ {
115
+ "code": f"artifact_parse_failed_{kind}",
116
+ "message": f"Artifact content parse failed for {kind}: {error}",
117
+ "field": "evidence.artifacts",
118
+ }
119
+ )
120
+
52
121
  hard_fail_codes = {"missing_artifacts", "missing_trace_ids"}
53
122
  if any(reason.get("code") in hard_fail_codes for reason in reasons):
54
123
  verdict = "fail"
@@ -66,19 +135,105 @@ def judge_claim(claim: dict[str, Any]) -> dict[str, Any]:
66
135
  "evidence": {
67
136
  "artifacts": artifacts,
68
137
  "trace_ids": trace_ids,
69
- "lineage": claim.get("lineage") if isinstance(claim.get("lineage"), dict) else {},
138
+ "lineage": normalized_claim.get("lineage") if isinstance(normalized_claim.get("lineage"), dict) else {},
70
139
  "security_scans": security_scans if isinstance(security_scans, list) else [],
71
140
  "browser_evidence": browser_evidence if isinstance(browser_evidence, list) else [],
72
141
  },
73
142
  }
74
143
 
75
144
 
145
+ def _normalize_claim(claim: dict[str, Any]) -> dict[str, Any]:
146
+ evidence = _as_dict(claim.get("evidence"))
147
+ artifact_refs = _as_non_empty_str_list(claim.get("artifacts"))
148
+ artifact_refs.extend(_normalize_artifact_records(evidence.get("artifacts")))
149
+
150
+ trace_ids = _as_non_empty_str_list(evidence.get("trace_ids"))
151
+ if not trace_ids:
152
+ trace_ids = _as_non_empty_str_list(claim.get("trace_ids"))
153
+
154
+ claim_lineage = claim.get("lineage")
155
+ lineage = claim_lineage if isinstance(claim_lineage, dict) else _as_dict(evidence.get("lineage"))
156
+
157
+ claim_security_scans = claim.get("security_scans")
158
+ security_scans = claim_security_scans if isinstance(claim_security_scans, list) else _as_non_empty_dict_list(evidence.get("security_scans"))
159
+
160
+ claim_browser_evidence = claim.get("browser_evidence")
161
+ browser_evidence = claim_browser_evidence if isinstance(claim_browser_evidence, list) else _as_non_empty_dict_list(evidence.get("browser_evidence"))
162
+
163
+ return {
164
+ "schema_version": claim.get("schema_version", 1),
165
+ "claim_type": claim.get("claim_type", ""),
166
+ "subject": claim.get("subject", ""),
167
+ "artifacts": artifact_refs,
168
+ "trace_ids": trace_ids,
169
+ "lineage": lineage,
170
+ "security_scans": security_scans,
171
+ "browser_evidence": browser_evidence,
172
+ }
173
+
174
+
175
+ def _normalize_artifact_records(value: Any) -> list[str]:
176
+ if not isinstance(value, list):
177
+ return []
178
+
179
+ refs: list[str] = []
180
+ for item in value:
181
+ if isinstance(item, str):
182
+ cleaned = item.strip()
183
+ if cleaned:
184
+ refs.append(cleaned)
185
+ continue
186
+ if not isinstance(item, dict):
187
+ continue
188
+ for field in ("kind", "path", "sha256", "parser", "summary", "trace_id"):
189
+ field_value = str(item.get(field, "")).strip()
190
+ if not field_value:
191
+ raise ValueError(f"claim_artifact_missing_{field}")
192
+ refs.append(str(item.get("path", "")).strip())
193
+ return refs
194
+
195
+
196
+ def parse_artifact_content(artifact: dict[str, Any], project_dir: str) -> dict[str, Any]:
197
+ kind = str(artifact.get("kind", "")).strip().lower()
198
+ path_value = str(artifact.get("path", "")).strip()
199
+ if not kind or not path_value:
200
+ return {"parsed": False, "kind": kind or "unknown", "summary": {}, "error": "missing_kind_or_path"}
201
+
202
+ parser = _PARSERS.get(kind)
203
+ if parser is None:
204
+ return {"parsed": False, "kind": kind, "summary": {}, "error": "unsupported_artifact_kind"}
205
+
206
+ file_path = Path(path_value)
207
+ if not file_path.is_absolute():
208
+ file_path = Path(project_dir) / file_path
209
+
210
+ parsed = parser(str(file_path))
211
+ return {
212
+ "parsed": bool(parsed.get("valid")),
213
+ "kind": kind,
214
+ "summary": parsed.get("summary", {}),
215
+ "error": parsed.get("error"),
216
+ }
217
+
218
+
76
219
  def _as_non_empty_str_list(value: Any) -> list[str]:
77
220
  if not isinstance(value, list):
78
221
  return []
79
222
  return [str(item).strip() for item in value if str(item).strip()]
80
223
 
81
224
 
225
+ def _as_non_empty_dict_list(value: Any) -> list[dict[str, Any]]:
226
+ if not isinstance(value, list):
227
+ return []
228
+ return [item for item in value if isinstance(item, dict)]
229
+
230
+
231
+ def _as_dict(value: Any) -> dict[str, Any]:
232
+ if not isinstance(value, dict):
233
+ return {}
234
+ return value
235
+
236
+
82
237
  def _has_failed_scan(value: Any) -> bool:
83
238
  if not isinstance(value, list):
84
239
  return False
@@ -93,3 +248,25 @@ def _has_failed_scan(value: Any) -> bool:
93
248
  if isinstance(unresolved_risks, list) and unresolved_risks:
94
249
  return True
95
250
  return False
251
+
252
+
253
+ def _extract_artifact_dicts(claim: dict[str, Any]) -> list[dict[str, Any]]:
254
+ evidence = _as_dict(claim.get("evidence"))
255
+ raw_artifacts = evidence.get("artifacts")
256
+ if not isinstance(raw_artifacts, list):
257
+ return []
258
+ return [item for item in raw_artifacts if isinstance(item, dict)]
259
+
260
+
261
+ _PARSERS: dict[str, Any] = {
262
+ "junit": artifact_parsers.parse_junit,
263
+ "sarif": artifact_parsers.parse_sarif,
264
+ "coverage": artifact_parsers.parse_coverage,
265
+ "browser_trace": artifact_parsers.parse_browser_trace,
266
+ "diff_hunk": artifact_parsers.parse_diff_hunk,
267
+ }
268
+
269
+
270
+ def _sanitize_run_id(value: str) -> str:
271
+ cleaned = "".join(ch if ch.isalnum() or ch in {"-", "_", "."} else "-" for ch in value.strip())
272
+ return cleaned or "unknown"
@@ -19,6 +19,7 @@ import zipfile
19
19
  import yaml
20
20
 
21
21
  from runtime.asset_loader import resolve_asset, resolve_assets
22
+ from runtime.proof_chain import _normalize_evidence_pack
22
23
  from runtime.adoption import (
23
24
  CANONICAL_MARKETPLACE_ID,
24
25
  CANONICAL_PACKAGE_NAME,
@@ -31,7 +32,7 @@ from runtime.adoption import (
31
32
  CONTRACT_DOC_PATH = Path("OMG_COMPAT_CONTRACT.md")
32
33
  SCHEMA_PATH = Path("registry") / "omg-capability.schema.json"
33
34
  BUNDLES_DIR = Path("registry") / "bundles"
34
- SUPPORTED_HOSTS = ("claude", "codex")
35
+ SUPPORTED_HOSTS = ("claude", "codex", "gemini", "kimi")
35
36
  SUPPORTED_CHANNELS = ("public", "enterprise")
36
37
  DEFAULT_REQUIRED_BUNDLES = (
37
38
  "control-plane",
@@ -120,6 +121,25 @@ REQUIRED_CODEX_OUTPUTS = (
120
121
  "codex-rules.md",
121
122
  "codex-mcp.toml",
122
123
  )
124
+ HOST_COMPILED_ARTIFACTS = {
125
+ "claude": (
126
+ ".claude-plugin/plugin.json",
127
+ ".claude-plugin/marketplace.json",
128
+ ".mcp.json",
129
+ "settings.json",
130
+ ),
131
+ "codex": (
132
+ ".agents/skills/omg/AGENTS.fragment.md",
133
+ ".agents/skills/omg/codex-rules.md",
134
+ ".agents/skills/omg/codex-mcp.toml",
135
+ ),
136
+ "gemini": (
137
+ ".gemini/settings.json",
138
+ ),
139
+ "kimi": (
140
+ ".kimi/mcp.json",
141
+ ),
142
+ }
123
143
 
124
144
 
125
145
  def _ensure_list(
@@ -170,7 +190,12 @@ def _validate_host_rule(
170
190
  )
171
191
 
172
192
 
173
- def _validate_policy_model(bundle_id: str, policy_model: Any) -> list[str]:
193
+ def _validate_policy_model(
194
+ bundle_id: str,
195
+ policy_model: Any,
196
+ *,
197
+ bundle_hosts: Iterable[str] = (),
198
+ ) -> list[str]:
174
199
  errors: list[str] = []
175
200
  payload = _ensure_dict(bundle_id=bundle_id, path="policy_model", value=policy_model, errors=errors)
176
201
  if not payload:
@@ -289,6 +314,8 @@ def _validate_policy_model(bundle_id: str, policy_model: Any) -> list[str]:
289
314
  value=payload.get("host_rules", {}),
290
315
  errors=errors,
291
316
  )
317
+ declared_hosts = {str(host).strip() for host in bundle_hosts if str(host).strip()}
318
+
292
319
  _validate_host_rule(
293
320
  bundle_id=bundle_id,
294
321
  host_name="claude",
@@ -303,6 +330,15 @@ def _validate_policy_model(bundle_id: str, policy_model: Any) -> list[str]:
303
330
  required_fields=("compilation_targets", "skills", "agents_fragments", "rules", "automations"),
304
331
  errors=errors,
305
332
  )
333
+ for host_name in ("gemini", "kimi"):
334
+ if host_name in host_rules or host_name in declared_hosts:
335
+ _validate_host_rule(
336
+ bundle_id=bundle_id,
337
+ host_name=host_name,
338
+ host_rule=host_rules.get(host_name),
339
+ required_fields=("compilation_targets", "mcp", "skills", "automations"),
340
+ errors=errors,
341
+ )
306
342
  return errors
307
343
 
308
344
 
@@ -461,7 +497,7 @@ def validate_contract_registry(root_dir: str | Path | None = None) -> dict[str,
461
497
  if bad_hosts:
462
498
  errors.append(f"{bundle_id}: unsupported hosts {bad_hosts}")
463
499
  if "policy_model" in bundle:
464
- errors.extend(_validate_policy_model(bundle_id, bundle.get("policy_model")))
500
+ errors.extend(_validate_policy_model(bundle_id, bundle.get("policy_model"), bundle_hosts=hosts))
465
501
 
466
502
  missing_bundles = [bundle_id for bundle_id in DEFAULT_REQUIRED_BUNDLES if bundle_id not in bundle_ids]
467
503
  for bundle_id in missing_bundles:
@@ -1145,6 +1181,34 @@ def _compile_codex_outputs(
1145
1181
  return artifacts
1146
1182
 
1147
1183
 
1184
+ def _compile_gemini_outputs(output_root: Path, channel: str) -> dict[str, Any]:
1185
+ del channel
1186
+ from runtime.mcp_config_writers import write_gemini_mcp_stdio_config
1187
+
1188
+ config_path = output_root / ".gemini" / "settings.json"
1189
+ write_gemini_mcp_stdio_config(
1190
+ command="python3",
1191
+ args=["-m", "runtime.omg_mcp_server"],
1192
+ server_name="omg-control",
1193
+ config_path=config_path,
1194
+ )
1195
+ return {"host": "gemini", "artifacts": [config_path]}
1196
+
1197
+
1198
+ def _compile_kimi_outputs(output_root: Path, channel: str) -> dict[str, Any]:
1199
+ del channel
1200
+ from runtime.mcp_config_writers import write_kimi_mcp_stdio_config
1201
+
1202
+ config_path = output_root / ".kimi" / "mcp.json"
1203
+ write_kimi_mcp_stdio_config(
1204
+ command="python3",
1205
+ args=["-m", "runtime.omg_mcp_server"],
1206
+ server_name="omg-control",
1207
+ config_path=config_path,
1208
+ )
1209
+ return {"host": "kimi", "artifacts": [config_path]}
1210
+
1211
+
1148
1212
  def _copy_release_bundle(
1149
1213
  *,
1150
1214
  output_root: Path,
@@ -1164,11 +1228,12 @@ def _copy_release_bundle(
1164
1228
  return copied
1165
1229
 
1166
1230
 
1167
- def _build_dist_manifest(output_root: Path, *, channel: str, artifacts: list[Path]) -> Path:
1231
+ def _build_dist_manifest(output_root: Path, *, channel: str, hosts: list[str], artifacts: list[Path]) -> Path:
1168
1232
  dist_root = output_root / "dist" / channel
1169
1233
  payload = {
1170
1234
  "schema": "OmgCompiledArtifactManifest",
1171
1235
  "channel": channel,
1236
+ "hosts": list(hosts),
1172
1237
  "contract_version": CANONICAL_VERSION,
1173
1238
  "artifacts": [
1174
1239
  {
@@ -1269,8 +1334,14 @@ def compile_contract_outputs(
1269
1334
  "artifacts": [],
1270
1335
  }
1271
1336
 
1337
+ if "gemini" in selected_hosts:
1338
+ artifacts.extend(_compile_gemini_outputs(output, channel)["artifacts"])
1339
+
1340
+ if "kimi" in selected_hosts:
1341
+ artifacts.extend(_compile_kimi_outputs(output, channel)["artifacts"])
1342
+
1272
1343
  bundled_artifacts = _copy_release_bundle(output_root=output, channel=channel, artifacts=artifacts)
1273
- manifest_path = _build_dist_manifest(output, channel=channel, artifacts=bundled_artifacts)
1344
+ manifest_path = _build_dist_manifest(output, channel=channel, hosts=selected_hosts, artifacts=bundled_artifacts)
1274
1345
  artifacts.append(manifest_path)
1275
1346
 
1276
1347
  return {
@@ -1291,7 +1362,7 @@ def _provider_statuses() -> dict[str, dict[str, Any]]:
1291
1362
  }
1292
1363
  statuses: dict[str, dict[str, Any]] = {}
1293
1364
 
1294
- for provider_name in ("claude", "codex"):
1365
+ for provider_name in SUPPORTED_HOSTS:
1295
1366
  if provider_name in ready_override:
1296
1367
  statuses[provider_name] = {"ready": True, "source": "env"}
1297
1368
  continue
@@ -1307,10 +1378,15 @@ def _provider_statuses() -> dict[str, dict[str, Any]]:
1307
1378
  }
1308
1379
  continue
1309
1380
 
1310
- import runtime.providers.codex_provider # noqa: F401
1381
+ if provider_name == "gemini":
1382
+ import runtime.providers.gemini_provider # noqa: F401
1383
+ elif provider_name == "kimi":
1384
+ import runtime.providers.kimi_provider # noqa: F401
1385
+ else:
1386
+ import runtime.providers.codex_provider # noqa: F401
1311
1387
  from runtime.cli_provider import get_provider
1312
1388
 
1313
- provider = get_provider("codex")
1389
+ provider = get_provider(provider_name)
1314
1390
  ready = bool(provider and provider.detect())
1315
1391
  statuses[provider_name] = {"ready": ready, "source": "provider"}
1316
1392
 
@@ -1557,6 +1633,12 @@ def _check_provider_host_parity(output_root: Path, providers: dict[str, dict[str
1557
1633
  output_root / ".agents" / "skills" / "omg" / "AGENTS.fragment.md",
1558
1634
  output_root / ".agents" / "skills" / "omg" / "codex-mcp.toml",
1559
1635
  ),
1636
+ "gemini": (
1637
+ output_root / ".gemini" / "settings.json",
1638
+ ),
1639
+ "kimi": (
1640
+ output_root / ".kimi" / "mcp.json",
1641
+ ),
1560
1642
  }
1561
1643
  for provider, status in providers.items():
1562
1644
  if not status.get("ready"):
@@ -1628,6 +1710,7 @@ def build_release_readiness(
1628
1710
  output = _resolve_output_root(root, output_root)
1629
1711
  blockers: list[str] = []
1630
1712
  checks: dict[str, Any] = {}
1713
+ required_provider_hosts: set[str] = set()
1631
1714
 
1632
1715
  validation = validate_contract_registry(root)
1633
1716
  checks["contract_validation"] = validation
@@ -1655,6 +1738,17 @@ def build_release_readiness(
1655
1738
  if _sha256_file(artifact_path) != expected_sha:
1656
1739
  manifest_errors.append(f"{required_channel}: sha mismatch for {rel_path}")
1657
1740
  manifest_paths = {str(a.get("path", "")) for a in manifest.get("artifacts", []) if isinstance(a, dict)}
1741
+ declared_hosts = [str(host) for host in manifest.get("hosts", []) if str(host).strip()]
1742
+ if not declared_hosts:
1743
+ declared_hosts = ["claude", "codex"]
1744
+ required_provider_hosts.update(declared_hosts)
1745
+ for host_name in declared_hosts:
1746
+ for host_path in HOST_COMPILED_ARTIFACTS.get(host_name, ()):
1747
+ bundled_host_path = f"bundle/{host_path}"
1748
+ if bundled_host_path not in manifest_paths:
1749
+ manifest_errors.append(
1750
+ f"{required_channel}: host_parity_missing {host_name} {bundled_host_path}"
1751
+ )
1658
1752
  for req_path in REQUIRED_ADVANCED_PLUGIN_ARTIFACTS:
1659
1753
  if req_path not in manifest_paths:
1660
1754
  manifest_errors.append(f"{required_channel}: advanced_plugin_missing {req_path}")
@@ -1739,10 +1833,17 @@ def build_release_readiness(
1739
1833
  providers = _provider_statuses()
1740
1834
  checks["providers"] = providers
1741
1835
  for provider_name, status in providers.items():
1836
+ if provider_name not in required_provider_hosts:
1837
+ continue
1742
1838
  if not status.get("ready"):
1743
1839
  blockers.append(f"provider not ready: {provider_name}")
1744
1840
 
1745
- provider_parity = _check_provider_host_parity(output, providers)
1841
+ required_providers = {
1842
+ provider_name: status
1843
+ for provider_name, status in providers.items()
1844
+ if provider_name in required_provider_hosts
1845
+ }
1846
+ provider_parity = _check_provider_host_parity(output, required_providers)
1746
1847
  checks["provider_host_parity"] = provider_parity
1747
1848
  blockers.extend(provider_parity.get("blockers", []))
1748
1849
 
@@ -1781,6 +1882,14 @@ def _check_recent_evidence(output_root: Path) -> dict[str, Any]:
1781
1882
  except Exception:
1782
1883
  continue
1783
1884
  if payload.get("schema") == "EvidencePack":
1885
+ try:
1886
+ payload = _normalize_evidence_pack(payload)
1887
+ except ValueError as exc:
1888
+ return {
1889
+ "status": "error",
1890
+ "evidence_file": str(path.relative_to(output_root)),
1891
+ "blockers": [f"invalid evidence pack: {exc}"],
1892
+ }
1784
1893
  evidence_payloads.append((path, payload))
1785
1894
 
1786
1895
  if not evidence_payloads:
@@ -0,0 +1,203 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import cast
6
+
7
+ JsonPrimitive = str | int | float | bool | None
8
+ JsonValue = JsonPrimitive | dict[str, "JsonValue"] | list["JsonValue"]
9
+ JsonObject = dict[str, JsonValue]
10
+
11
+
12
+ _EVIDENCE_DIRS = (
13
+ Path(".omg") / "evidence",
14
+ Path(".omg") / "tracebank",
15
+ Path(".omg") / "evals",
16
+ Path(".omg") / "lineage",
17
+ Path(".omg") / "state",
18
+ )
19
+
20
+
21
+ def _iter_json_files(root: Path, rel_dir: Path) -> list[Path]:
22
+ directory = root / rel_dir
23
+ if not directory.exists():
24
+ return []
25
+ return sorted(path for path in directory.glob("*.json") if path.is_file())
26
+
27
+
28
+ def _load_json(path: Path) -> JsonObject | None:
29
+ try:
30
+ payload: object = json.loads(path.read_text(encoding="utf-8")) # pyright: ignore[reportAny]
31
+ except (OSError, json.JSONDecodeError):
32
+ return None
33
+ if not isinstance(payload, dict):
34
+ return None
35
+ return cast(JsonObject, payload)
36
+
37
+
38
+ def _read_jsonl(path: Path) -> list[JsonObject]:
39
+ if not path.exists():
40
+ return []
41
+ rows: list[JsonObject] = []
42
+ try:
43
+ for line in path.read_text(encoding="utf-8").splitlines():
44
+ line = line.strip()
45
+ if not line:
46
+ continue
47
+ try:
48
+ item: object = json.loads(line) # pyright: ignore[reportAny]
49
+ except json.JSONDecodeError:
50
+ continue
51
+ if isinstance(item, dict):
52
+ rows.append(cast(JsonObject, item))
53
+ except OSError:
54
+ return []
55
+ return rows
56
+
57
+
58
+ def _record_string(record: JsonObject, key: str) -> str:
59
+ value = record.get(key)
60
+ return value if isinstance(value, str) else ""
61
+
62
+
63
+ def _record_string_list(record: JsonObject, key: str) -> list[str]:
64
+ value = record.get(key)
65
+ if not isinstance(value, list):
66
+ return []
67
+ return [item for item in value if isinstance(item, str)]
68
+
69
+
70
+ def _record_matches(
71
+ record: JsonObject,
72
+ *,
73
+ run_id: str | None,
74
+ trace_id: str | None,
75
+ schema: str | None,
76
+ kind: str | None,
77
+ ) -> bool:
78
+ if run_id is not None and _record_string(record, "run_id") != run_id:
79
+ return False
80
+ if trace_id is not None:
81
+ direct_trace_id = _record_string(record, "trace_id")
82
+ trace_ids = _record_string_list(record, "trace_ids")
83
+ if direct_trace_id != trace_id and trace_id not in trace_ids:
84
+ return False
85
+ if schema is not None and _record_string(record, "schema") != schema:
86
+ return False
87
+ if kind is not None:
88
+ direct_kind = _record_string(record, "kind")
89
+ artifacts = record.get("artifacts")
90
+ artifact_kinds: list[str] = []
91
+ if isinstance(artifacts, list):
92
+ for item in artifacts:
93
+ if isinstance(item, dict):
94
+ item_kind = item.get("kind")
95
+ if isinstance(item_kind, str):
96
+ artifact_kinds.append(item_kind)
97
+ if direct_kind != kind and kind not in artifact_kinds:
98
+ return False
99
+ return True
100
+
101
+
102
+ def get_evidence_pack(project_dir: str, run_id: str) -> JsonObject | None:
103
+ root = Path(project_dir)
104
+ evidence_files = _iter_json_files(root, Path(".omg") / "evidence")
105
+ for path in evidence_files:
106
+ payload = _load_json(path)
107
+ if payload is None:
108
+ continue
109
+ if payload.get("schema") != "EvidencePack":
110
+ continue
111
+ if _record_string(payload, "run_id") == run_id:
112
+ return payload
113
+ return None
114
+
115
+
116
+ def query_evidence(
117
+ project_dir: str,
118
+ *,
119
+ run_id: str | None = None,
120
+ trace_id: str | None = None,
121
+ schema: str | None = None,
122
+ kind: str | None = None,
123
+ ) -> list[JsonObject]:
124
+ root = Path(project_dir)
125
+ records: list[JsonObject] = []
126
+
127
+ for rel_dir in _EVIDENCE_DIRS:
128
+ for path in _iter_json_files(root, rel_dir):
129
+ payload = _load_json(path)
130
+ if payload is None:
131
+ continue
132
+ if _record_matches(
133
+ payload,
134
+ run_id=run_id,
135
+ trace_id=trace_id,
136
+ schema=schema,
137
+ kind=kind,
138
+ ):
139
+ records.append(payload)
140
+
141
+ for row in _read_jsonl(root / rel_dir / "events.jsonl"):
142
+ if _record_matches(
143
+ row,
144
+ run_id=run_id,
145
+ trace_id=trace_id,
146
+ schema=schema,
147
+ kind=kind,
148
+ ):
149
+ records.append(row)
150
+
151
+ return records
152
+
153
+
154
+ def list_evidence_packs(project_dir: str) -> list[JsonObject]:
155
+ root = Path(project_dir)
156
+ evidence_files = _iter_json_files(root, Path(".omg") / "evidence")
157
+ payloads: list[tuple[float, JsonObject]] = []
158
+
159
+ for path in evidence_files:
160
+ payload = _load_json(path)
161
+ if payload is None or payload.get("schema") != "EvidencePack":
162
+ continue
163
+ try:
164
+ mtime = path.stat().st_mtime
165
+ except OSError:
166
+ mtime = 0.0
167
+ payloads.append((mtime, payload))
168
+
169
+ payloads.sort(key=lambda item: item[0], reverse=True)
170
+ return [payload for _, payload in payloads]
171
+
172
+
173
+ def get_trace(project_dir: str, trace_id: str) -> JsonObject | None:
174
+ trace_rows = _read_jsonl(Path(project_dir) / ".omg" / "tracebank" / "events.jsonl")
175
+ for row in trace_rows:
176
+ if _record_string(row, "trace_id") == trace_id:
177
+ return row
178
+ return None
179
+
180
+
181
+ def get_eval(project_dir: str) -> JsonObject | None:
182
+ return _load_json(Path(project_dir) / ".omg" / "evals" / "latest.json")
183
+
184
+
185
+ def get_lineage(project_dir: str, lineage_id: str) -> JsonObject | None:
186
+ root = Path(project_dir)
187
+ lineage_files = _iter_json_files(root, Path(".omg") / "lineage")
188
+ for path in lineage_files:
189
+ payload = _load_json(path)
190
+ if payload is None:
191
+ continue
192
+ if _record_string(payload, "lineage_id") == lineage_id:
193
+ return payload
194
+ return None
195
+
196
+
197
+ def get_verification_state(project_dir: str) -> JsonObject | None:
198
+ payload = _load_json(Path(project_dir) / ".omg" / "state" / "background-verification.json")
199
+ if payload is None:
200
+ return None
201
+ if payload.get("schema") != "BackgroundVerificationState":
202
+ return None
203
+ return payload