@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
@@ -0,0 +1,292 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ import hashlib
5
+ import json
6
+ from pathlib import Path
7
+ from typing import cast
8
+
9
+ from runtime.evidence_query import (
10
+ JsonObject,
11
+ JsonValue,
12
+ get_eval,
13
+ get_evidence_pack,
14
+ get_lineage,
15
+ get_trace,
16
+ get_verification_state,
17
+ )
18
+
19
+
20
+ def _now() -> str:
21
+ return datetime.now(timezone.utc).isoformat()
22
+
23
+
24
+ def _load_json(path: Path) -> JsonObject | None:
25
+ try:
26
+ payload: object = json.loads(path.read_text(encoding="utf-8")) # pyright: ignore[reportAny]
27
+ except (OSError, json.JSONDecodeError):
28
+ return None
29
+ if not isinstance(payload, dict):
30
+ return None
31
+ return cast(JsonObject, payload)
32
+
33
+
34
+ def _hash_path(path: Path) -> str:
35
+ if not path.exists() or not path.is_file():
36
+ return ""
37
+ digest = hashlib.sha256()
38
+ with path.open("rb") as handle:
39
+ while True:
40
+ chunk = handle.read(8192)
41
+ if not chunk:
42
+ break
43
+ digest.update(chunk)
44
+ return digest.hexdigest()
45
+
46
+
47
+ def _rel(path: Path, root: Path) -> str:
48
+ try:
49
+ return path.resolve().relative_to(root.resolve()).as_posix()
50
+ except ValueError:
51
+ return path.as_posix()
52
+
53
+
54
+ def _as_object_list(value: JsonValue | None) -> list[JsonObject]:
55
+ if not isinstance(value, list):
56
+ return []
57
+ return [item for item in value if isinstance(item, dict)]
58
+
59
+
60
+ def _as_string_list(value: JsonValue | None) -> list[str]:
61
+ if not isinstance(value, list):
62
+ return []
63
+ return [item for item in value if isinstance(item, str)]
64
+
65
+
66
+ def _string_field(payload: JsonObject, key: str) -> str:
67
+ value = payload.get(key)
68
+ return value if isinstance(value, str) else ""
69
+
70
+
71
+ def _artifact_ref(*, kind: str, path: str, sha256: str = "", extras: JsonObject | None = None) -> JsonObject:
72
+ artifact: JsonObject = {
73
+ "kind": kind,
74
+ "path": path,
75
+ "sha256": sha256,
76
+ }
77
+ if extras:
78
+ artifact.update(extras)
79
+ return artifact
80
+
81
+
82
+ def _trace_reference(root: Path, trace_ids: list[str]) -> JsonObject | None:
83
+ trace_path = root / ".omg" / "tracebank" / "events.jsonl"
84
+ if not trace_path.exists():
85
+ return None
86
+
87
+ requested = sorted({trace_id for trace_id in trace_ids if trace_id})
88
+ matched: list[str] = []
89
+ matched_count = 0
90
+ try:
91
+ for line in trace_path.read_text(encoding="utf-8").splitlines():
92
+ line = line.strip()
93
+ if not line:
94
+ continue
95
+ try:
96
+ row: object = json.loads(line) # pyright: ignore[reportAny]
97
+ except json.JSONDecodeError:
98
+ continue
99
+ if not isinstance(row, dict):
100
+ continue
101
+ row_payload = cast(JsonObject, row)
102
+ trace_id = _string_field(row_payload, "trace_id")
103
+ if trace_id in requested:
104
+ matched_count += 1
105
+ if trace_id not in matched:
106
+ matched.append(trace_id)
107
+ except OSError:
108
+ return None
109
+
110
+ return _artifact_ref(
111
+ kind="trace_events",
112
+ path=".omg/tracebank/events.jsonl",
113
+ sha256=_hash_path(trace_path),
114
+ extras=cast(JsonObject, {"trace_ids": sorted(matched), "event_count": matched_count}),
115
+ )
116
+
117
+
118
+ def _find_lineage_path(root: Path, lineage_id: str) -> str:
119
+ if not lineage_id:
120
+ return ""
121
+ lineage_dir = root / ".omg" / "lineage"
122
+ if not lineage_dir.exists():
123
+ return ""
124
+
125
+ for path in sorted(lineage_dir.glob("*.json")):
126
+ payload = _load_json(path)
127
+ if payload is None:
128
+ continue
129
+ if _string_field(payload, "lineage_id") == lineage_id:
130
+ return _rel(path, root)
131
+ return ""
132
+
133
+
134
+ def _security_artifacts(root: Path, scans: JsonValue | None) -> list[JsonObject]:
135
+ artifacts: list[JsonObject] = []
136
+ for item in _as_object_list(scans):
137
+ path = _string_field(item, "path").strip()
138
+ if not path:
139
+ continue
140
+ artifacts.append(
141
+ _artifact_ref(
142
+ kind="security_evidence",
143
+ path=path,
144
+ sha256=_hash_path(root / path),
145
+ extras={"schema": _string_field(item, "schema")},
146
+ )
147
+ )
148
+ return artifacts
149
+
150
+
151
+ def _browser_artifacts(root: Path, records: JsonValue | None) -> list[JsonObject]:
152
+ artifacts: list[JsonObject] = []
153
+ for item in _as_object_list(records):
154
+ if _string_field(item, "kind") != "browser_trace":
155
+ continue
156
+ path = _string_field(item, "path").strip()
157
+ if not path:
158
+ continue
159
+ artifacts.append(
160
+ _artifact_ref(
161
+ kind="browser_trace",
162
+ path=path,
163
+ sha256=_hash_path(root / path),
164
+ extras={"trace_id": _string_field(item, "trace_id")},
165
+ )
166
+ )
167
+ return artifacts
168
+
169
+
170
+ def _incident_artifacts(root: Path, records: JsonValue | None) -> list[JsonObject]:
171
+ artifacts: list[JsonObject] = []
172
+ for item in _as_object_list(records):
173
+ kind = _string_field(item, "kind")
174
+ if "incident" not in kind:
175
+ continue
176
+ path = _string_field(item, "path").strip()
177
+ if not path:
178
+ continue
179
+ artifacts.append(
180
+ _artifact_ref(
181
+ kind=kind,
182
+ path=path,
183
+ sha256=_hash_path(root / path),
184
+ extras={"trace_id": _string_field(item, "trace_id")},
185
+ )
186
+ )
187
+ return artifacts
188
+
189
+
190
+ def _dedupe_artifacts(artifacts: list[JsonObject]) -> list[JsonObject]:
191
+ seen: set[tuple[str, str, str]] = set()
192
+ deduped: list[JsonObject] = []
193
+ sorted_items = sorted(artifacts, key=lambda item: (_string_field(item, "kind"), _string_field(item, "path")))
194
+ for artifact in sorted_items:
195
+ key = (
196
+ _string_field(artifact, "kind"),
197
+ _string_field(artifact, "path"),
198
+ _string_field(artifact, "sha256"),
199
+ )
200
+ if key in seen:
201
+ continue
202
+ seen.add(key)
203
+ deduped.append(artifact)
204
+ return deduped
205
+
206
+
207
+ def build_repro_pack(project_dir: str, run_id: str) -> dict[str, str]:
208
+ root = Path(project_dir)
209
+ evidence_pack = get_evidence_pack(project_dir, run_id)
210
+ if evidence_pack is None:
211
+ return {
212
+ "status": "error",
213
+ "run_id": run_id,
214
+ "reason": "evidence_pack_not_found",
215
+ }
216
+
217
+ evidence_pack_path = f".omg/evidence/{run_id}.json"
218
+ artifacts: list[JsonObject] = [
219
+ _artifact_ref(kind="evidence_pack", path=evidence_pack_path, sha256=_hash_path(root / evidence_pack_path))
220
+ ]
221
+
222
+ trace_ids = sorted(set(_as_string_list(evidence_pack.get("trace_ids"))))
223
+ trace_file = root / ".omg" / "tracebank" / "events.jsonl"
224
+ for trace_id in trace_ids:
225
+ if get_trace(project_dir, trace_id) is None:
226
+ continue
227
+ artifacts.append(
228
+ _artifact_ref(
229
+ kind="trace",
230
+ path=".omg/tracebank/events.jsonl",
231
+ sha256=_hash_path(trace_file),
232
+ extras={"trace_id": trace_id},
233
+ )
234
+ )
235
+ trace_reference = _trace_reference(root, trace_ids)
236
+ if trace_reference is not None:
237
+ artifacts.append(trace_reference)
238
+
239
+ eval_path = root / ".omg" / "evals" / "latest.json"
240
+ if get_eval(project_dir) is not None and eval_path.exists():
241
+ artifacts.append(_artifact_ref(kind="eval", path=".omg/evals/latest.json", sha256=_hash_path(eval_path)))
242
+
243
+ lineage_payload = evidence_pack.get("lineage")
244
+ lineage_id = ""
245
+ if isinstance(lineage_payload, dict):
246
+ lineage_payload_obj = cast(JsonObject, lineage_payload)
247
+ lineage_id = _string_field(lineage_payload_obj, "lineage_id")
248
+ if lineage_id and get_lineage(project_dir, lineage_id) is not None:
249
+ lineage_path = _find_lineage_path(root, lineage_id)
250
+ if lineage_path:
251
+ artifacts.append(
252
+ _artifact_ref(
253
+ kind="lineage",
254
+ path=lineage_path,
255
+ sha256=_hash_path(root / lineage_path),
256
+ extras={"lineage_id": lineage_id},
257
+ )
258
+ )
259
+
260
+ artifacts.extend(_security_artifacts(root, evidence_pack.get("security_scans")))
261
+ artifacts.extend(_browser_artifacts(root, evidence_pack.get("artifacts")))
262
+ artifacts.extend(_incident_artifacts(root, evidence_pack.get("artifacts")))
263
+
264
+ verification_path = root / ".omg" / "state" / "background-verification.json"
265
+ if get_verification_state(project_dir) is not None and verification_path.exists():
266
+ artifacts.append(
267
+ _artifact_ref(
268
+ kind="verification_state",
269
+ path=".omg/state/background-verification.json",
270
+ sha256=_hash_path(verification_path),
271
+ )
272
+ )
273
+
274
+ unresolved_risks = _as_string_list(evidence_pack.get("unresolved_risks"))
275
+ manifest: dict[str, object] = {
276
+ "schema": "ReproPack",
277
+ "schema_version": 1,
278
+ "run_id": run_id,
279
+ "evidence_pack_path": evidence_pack_path,
280
+ "artifacts": _dedupe_artifacts(artifacts),
281
+ "unresolved_risks": unresolved_risks,
282
+ "assembled_at": _now(),
283
+ }
284
+
285
+ out_path = root / ".omg" / "evidence" / f"repro-pack-{run_id}.json"
286
+ out_path.parent.mkdir(parents=True, exist_ok=True)
287
+ _ = out_path.write_text(json.dumps(manifest, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
288
+ return {
289
+ "status": "ok",
290
+ "run_id": run_id,
291
+ "path": _rel(out_path, root),
292
+ }
@@ -2,31 +2,93 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  from pathlib import Path
5
- from typing import Any
5
+ from typing import TypedDict, cast
6
6
 
7
7
  import yaml
8
8
 
9
+ from .adoption import CANONICAL_MODE_NAMES
9
10
 
10
- PROFILE_PRESETS: dict[str, dict[str, Any]] = {
11
+
12
+ class RuntimeProfile(TypedDict):
13
+ profile: str
14
+ max_workers: int
15
+ background_polling: bool
16
+
17
+
18
+ class CanonicalModeProfile(TypedDict):
19
+ concurrency: int
20
+ background_verification: bool
21
+ context_window: str
22
+ noise_level: str
23
+
24
+
25
+ PROFILE_PRESETS: dict[str, RuntimeProfile] = {
11
26
  "eco": {"profile": "eco", "max_workers": 2, "background_polling": False},
12
27
  "balanced": {"profile": "balanced", "max_workers": 3, "background_polling": False},
13
28
  "turbo": {"profile": "turbo", "max_workers": 5, "background_polling": True},
14
29
  }
15
30
 
31
+ RUNTIME_CONCURRENCY_PROFILE_NAMES = tuple(PROFILE_PRESETS.keys())
32
+ RESERVED_CANONICAL_MODE_NAMES = CANONICAL_MODE_NAMES
33
+
34
+ _CANONICAL_MODE_PROFILES: dict[str, CanonicalModeProfile] = {
35
+ "chill": {
36
+ "concurrency": 1,
37
+ "background_verification": False,
38
+ "context_window": "minimal",
39
+ "noise_level": "quiet",
40
+ },
41
+ "focused": {
42
+ "concurrency": 2,
43
+ "background_verification": False,
44
+ "context_window": "standard",
45
+ "noise_level": "normal",
46
+ },
47
+ "exploratory": {
48
+ "concurrency": 4,
49
+ "background_verification": True,
50
+ "context_window": "extended",
51
+ "noise_level": "verbose",
52
+ },
53
+ }
54
+
55
+
56
+ def load_canonical_mode_profile(mode: str) -> dict[str, object]:
57
+ normalized_mode = mode.strip().lower()
58
+ profile = _CANONICAL_MODE_PROFILES.get(normalized_mode)
59
+ if profile is None:
60
+ raise ValueError(
61
+ f"Unknown canonical mode: {mode!r}. Valid: chill, focused, exploratory"
62
+ )
63
+ return dict(profile)
16
64
 
17
- def load_runtime_profile(project_dir: str) -> dict[str, Any]:
65
+
66
+ def load_runtime_profile(project_dir: str) -> RuntimeProfile:
18
67
  runtime_path = Path(project_dir) / ".omg" / "runtime.yaml"
19
68
  profile_name = "balanced"
20
69
  if runtime_path.exists():
21
70
  try:
22
- payload = yaml.safe_load(runtime_path.read_text(encoding="utf-8")) or {}
71
+ raw_payload: object = yaml.safe_load(runtime_path.read_text(encoding="utf-8")) or {}
23
72
  except Exception:
24
- payload = {}
25
- if isinstance(payload, dict):
26
- candidate = str(payload.get("profile", profile_name)).strip()
27
- if candidate in PROFILE_PRESETS:
73
+ raw_payload = {}
74
+ if isinstance(raw_payload, dict):
75
+ payload_map = cast(dict[object, object], raw_payload)
76
+ payload: dict[str, object] = {}
77
+ for key, value in payload_map.items():
78
+ if isinstance(key, str):
79
+ payload[key] = value
80
+ candidate_obj = payload.get("profile", profile_name)
81
+ candidate = candidate_obj.strip() if isinstance(candidate_obj, str) else profile_name
82
+ if candidate in RUNTIME_CONCURRENCY_PROFILE_NAMES:
28
83
  profile_name = candidate
29
- return dict(PROFILE_PRESETS[profile_name])
84
+
85
+ preset = PROFILE_PRESETS[profile_name]
86
+ result: RuntimeProfile = {
87
+ "profile": preset["profile"],
88
+ "max_workers": preset["max_workers"],
89
+ "background_polling": preset["background_polling"],
90
+ }
91
+ return result
30
92
 
31
93
 
32
94
  def resolve_parallel_workers(project_dir: str, *, requested_workers: int) -> int:
@@ -43,19 +105,29 @@ def _load_cli_parallel_cap(project_dir: str) -> int | None:
43
105
  if not config_path.exists():
44
106
  return None
45
107
  try:
46
- payload = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
108
+ raw_payload: object = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
47
109
  except Exception:
48
110
  return None
49
- if not isinstance(payload, dict):
111
+ if not isinstance(raw_payload, dict):
50
112
  return None
51
- cli_configs = payload.get("cli_configs", {})
52
- if not isinstance(cli_configs, dict):
113
+
114
+ payload_map = cast(dict[object, object], raw_payload)
115
+ payload: dict[str, object] = {}
116
+ for key, value in payload_map.items():
117
+ if isinstance(key, str):
118
+ payload[key] = value
119
+
120
+ cli_configs_obj = payload.get("cli_configs")
121
+ if not isinstance(cli_configs_obj, dict):
53
122
  return None
54
- caps = []
123
+
124
+ cli_configs = cast(dict[object, object], cli_configs_obj)
125
+ caps: list[int] = []
55
126
  for config in cli_configs.values():
56
127
  if not isinstance(config, dict):
57
128
  continue
58
- value = config.get("max_parallel_agents")
129
+ config_map = cast(dict[object, object], config)
130
+ value = config_map.get("max_parallel_agents")
59
131
  if isinstance(value, int) and value > 0:
60
132
  caps.append(value)
61
133
  return min(caps) if caps else None
@@ -8,6 +8,7 @@ from hashlib import sha256
8
8
  import json
9
9
  from pathlib import Path
10
10
  import re
11
+ import shutil
11
12
  import subprocess
12
13
  from typing import Any
13
14
 
@@ -128,6 +129,8 @@ def run_security_check(
128
129
  "severity": finding.get("severity"),
129
130
  "exploitability": finding.get("exploitability", "unknown"),
130
131
  "reachability": finding.get("reachability", "unknown"),
132
+ "kev_listed": finding.get("kev_listed", False),
133
+ "epss_score": finding.get("epss_score"),
131
134
  "waived": bool(finding.get("waived")),
132
135
  "waiver_justification": finding.get("waiver_justification", ""),
133
136
  "message": finding.get("message", ""),
@@ -267,9 +270,89 @@ def _scan_python_ast(scope_path: Path) -> list[dict[str, Any]]:
267
270
  continue
268
271
  findings.extend(_scan_python_file(py_file, source))
269
272
  findings.extend(_run_bandit_if_available(scope_path))
273
+ findings.extend(_scan_semgrep(scope_path))
270
274
  return findings
271
275
 
272
276
 
277
+ def run_semgrep_scan(project_dir: str, rules: str = "auto") -> dict[str, Any]:
278
+ unavailable = {"status": "unavailable", "findings": [], "error": "semgrep not found"}
279
+ if shutil.which("semgrep") is None:
280
+ return unavailable
281
+
282
+ cmd = ["semgrep", "--json", "--config", rules, project_dir]
283
+ try:
284
+ proc = subprocess.run(cmd, capture_output=True, text=True, check=False, timeout=60)
285
+ except Exception:
286
+ return unavailable
287
+
288
+ if proc.returncode not in {0, 1}:
289
+ return unavailable
290
+
291
+ try:
292
+ payload = json.loads(proc.stdout or "{}")
293
+ except Exception:
294
+ return unavailable
295
+
296
+ findings: list[dict[str, Any]] = []
297
+ for item in payload.get("results", []):
298
+ extra = item.get("extra") if isinstance(item.get("extra"), dict) else {}
299
+ start = item.get("start") if isinstance(item.get("start"), dict) else {}
300
+ findings.append(
301
+ {
302
+ "severity": _normalize_semgrep_severity(str(extra.get("severity", "WARNING"))),
303
+ "rule": str(item.get("check_id", "semgrep")),
304
+ "path": str(item.get("path", "")),
305
+ "line": _safe_int(start.get("line", 1), default=1),
306
+ "message": str(extra.get("message", "Semgrep finding")),
307
+ }
308
+ )
309
+ return {"status": "ok", "findings": findings, "error": ""}
310
+
311
+
312
+ def _normalize_semgrep_severity(raw: str) -> str:
313
+ lowered = raw.lower()
314
+ if lowered in {"error", "critical"}:
315
+ return "high"
316
+ if lowered in {"warning", "warn"}:
317
+ return "medium"
318
+ if lowered in {"info", "note", "low"}:
319
+ return "low"
320
+ return _normalize_severity(lowered)
321
+
322
+
323
+ def _scan_semgrep(scope_path: Path) -> list[dict[str, Any]]:
324
+ result = run_semgrep_scan(str(scope_path))
325
+ if result.get("status") != "ok":
326
+ return []
327
+
328
+ findings: list[dict[str, Any]] = []
329
+ for item in result.get("findings", []):
330
+ if not isinstance(item, dict):
331
+ continue
332
+ file_path = Path(str(item.get("path", "")))
333
+ findings.append(
334
+ _finding(
335
+ rule_id=str(item.get("rule", "semgrep")),
336
+ source_name="semgrep-ce",
337
+ category="python_ast",
338
+ severity=_normalize_severity(str(item.get("severity", "medium"))),
339
+ path=file_path,
340
+ line=_safe_int(item.get("line", 1), default=1),
341
+ message=str(item.get("message", "Semgrep finding")),
342
+ recommendation="Review Semgrep finding and apply the suggested remediation.",
343
+ snippet="",
344
+ )
345
+ )
346
+ return findings
347
+
348
+
349
+ def _safe_int(value: Any, *, default: int) -> int:
350
+ try:
351
+ return int(value)
352
+ except (TypeError, ValueError):
353
+ return default
354
+
355
+
273
356
  def _scan_secret_patterns(scope_path: Path) -> list[dict[str, Any]]:
274
357
  findings: list[dict[str, Any]] = []
275
358
  for candidate in _iter_text_candidates(scope_path):
@@ -488,9 +571,7 @@ def _run_bandit_if_available(scope_path: Path) -> list[dict[str, Any]]:
488
571
 
489
572
 
490
573
  def _command_exists(command: str) -> bool:
491
- from shutil import which
492
-
493
- return which(command) is not None
574
+ return shutil.which(command) is not None
494
575
 
495
576
 
496
577
  def _scan_dependency_health(scope_path: Path, include_live_enrichment: bool) -> list[dict[str, Any]]:
@@ -530,6 +611,8 @@ def _scan_dependency_health(scope_path: Path, include_live_enrichment: bool) ->
530
611
  "severity": _normalize_severity(str(vuln.get("severity", "unknown"))),
531
612
  "exploitability": _risk_to_exploitability(str(reachability.get("risk_level", ""))),
532
613
  "reachability": _normalize_reachability(str(reachability.get("reachability", "unknown"))),
614
+ "kev_listed": reachability.get("kev_listed", False),
615
+ "epss_score": reachability.get("epss_score"),
533
616
  "evidence": {
534
617
  "package": package_name,
535
618
  "version": dependency["version"],
@@ -1,6 +1,47 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
4
+ from pathlib import Path
3
5
  from typing import Any
6
+ from uuid import uuid4
7
+
8
+
9
+ def lock_intent(project_dir: str, intent: dict[str, Any]) -> dict[str, Any]:
10
+ lock_id = str(uuid4())
11
+ lock_dir = Path(project_dir) / ".omg" / "state" / "test-intent-lock"
12
+ lock_dir.mkdir(parents=True, exist_ok=True)
13
+
14
+ lock_path = lock_dir / f"{lock_id}.json"
15
+ payload = {"schema": "TestIntentLock", "lock_id": lock_id, "intent": intent}
16
+ lock_path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
17
+
18
+ return {"lock_id": lock_id, "status": "locked", "path": str(lock_path)}
19
+
20
+
21
+ def verify_intent(project_dir: str, lock_id: str, results: dict[str, Any]) -> dict[str, Any]:
22
+ lock_path = Path(project_dir) / ".omg" / "state" / "test-intent-lock" / f"{lock_id}.json"
23
+ if not lock_path.exists():
24
+ return {"status": "missing_lock", "lock_id": lock_id, "reasons": ["missing lock state"]}
25
+
26
+ try:
27
+ payload = json.loads(lock_path.read_text(encoding="utf-8"))
28
+ except (OSError, json.JSONDecodeError):
29
+ return {"status": "missing_lock", "lock_id": lock_id, "reasons": ["missing lock state"]}
30
+
31
+ intent = payload.get("intent") if isinstance(payload, dict) else {}
32
+ intent_tests = _normalize_string_list(intent.get("tests") if isinstance(intent, dict) else None)
33
+ result_tests = _normalize_string_list(results.get("tests"))
34
+ weakened_assertions = results.get("weakened_assertions")
35
+
36
+ reasons: list[str] = []
37
+ if isinstance(weakened_assertions, list) and weakened_assertions:
38
+ reasons.append("weakened_assertions_present")
39
+
40
+ if result_tests != intent_tests:
41
+ reasons.append("tests_mismatch")
42
+
43
+ status = "ok" if not reasons else "fail"
44
+ return {"status": status, "lock_id": lock_id, "reasons": reasons}
4
45
 
5
46
 
6
47
  def evaluate_test_delta(delta: dict[str, Any]) -> dict[str, Any]:
@@ -89,3 +130,9 @@ def _normalize_tests(value: Any) -> list[dict[str, Any]]:
89
130
  }
90
131
  )
91
132
  return tests
133
+
134
+
135
+ def _normalize_string_list(value: Any) -> list[str]:
136
+ if not isinstance(value, list):
137
+ return []
138
+ return [str(item).strip() for item in value if str(item).strip()]
@@ -40,6 +40,7 @@ def record_trace(
40
40
  trace_type: str,
41
41
  route: str,
42
42
  status: str,
43
+ schema_version: int | None = None,
43
44
  plan: dict[str, Any] | None = None,
44
45
  patch: dict[str, Any] | None = None,
45
46
  verify: dict[str, Any] | None = None,
@@ -49,7 +50,7 @@ def record_trace(
49
50
  ) -> dict[str, Any]:
50
51
  trace_id = f"trace-{uuid4().hex}"
51
52
  timestamp = _now()
52
- record = {
53
+ record: dict[str, Any] = {
53
54
  "schema": "TracebankRecord",
54
55
  "trace_id": trace_id,
55
56
  "timestamp": timestamp,
@@ -66,6 +67,10 @@ def record_trace(
66
67
  "rejections": rejections or [],
67
68
  "metadata": metadata or {},
68
69
  }
70
+ if schema_version is not None:
71
+ record["schema_version"] = schema_version
72
+ elif isinstance(metadata, dict) and metadata.get("schema_version") is not None:
73
+ record["schema_version"] = metadata.get("schema_version")
69
74
 
70
75
  path = Path(project_dir) / TRACEBANK_REL_PATH
71
76
  path.parent.mkdir(parents=True, exist_ok=True)
@@ -73,11 +78,34 @@ def record_trace(
73
78
  _ = handle.write(json.dumps(record, ensure_ascii=True) + "\n")
74
79
 
75
80
  record["path"] = TRACEBANK_REL_PATH.as_posix()
81
+
82
+ verification_status = (metadata or {}).get("verification_status")
83
+ if verification_status:
84
+ try:
85
+ from runtime.background_verification import publish_verification_state
86
+
87
+ publish_verification_state(
88
+ project_dir=project_dir,
89
+ run_id=trace_id,
90
+ status=str(verification_status),
91
+ blockers=(metadata or {}).get("verification_blockers", []),
92
+ evidence_links=(metadata or {}).get("verification_evidence_links", []),
93
+ progress=(metadata or {}).get("verification_progress", {}),
94
+ )
95
+ except Exception:
96
+ pass
97
+
76
98
  return record
77
99
 
78
100
 
79
- def link_evidence(project_dir: str, *, trace_id: str, evidence_path: str) -> dict[str, Any]:
80
- link = {
101
+ def link_evidence(
102
+ project_dir: str,
103
+ *,
104
+ trace_id: str,
105
+ evidence_path: str,
106
+ schema_version: int | None = None,
107
+ ) -> dict[str, Any]:
108
+ link: dict[str, Any] = {
81
109
  "schema": "TraceEvidenceLink",
82
110
  "trace_id": trace_id,
83
111
  "evidence_path": evidence_path,
@@ -85,6 +113,8 @@ def link_evidence(project_dir: str, *, trace_id: str, evidence_path: str) -> dic
85
113
  "executor": _executor(),
86
114
  "environment": _environment(),
87
115
  }
116
+ if schema_version is not None:
117
+ link["schema_version"] = schema_version
88
118
 
89
119
  path = Path(project_dir) / TRACEBANK_EVIDENCE_LINKS_REL_PATH
90
120
  path.parent.mkdir(parents=True, exist_ok=True)