@trac3er/oh-my-god 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/.claude-plugin/marketplace.json +8 -8
  2. package/.claude-plugin/plugin.json +5 -4
  3. package/.claude-plugin/scripts/uninstall.sh +74 -3
  4. package/.claude-plugin/scripts/update.sh +78 -3
  5. package/.coveragerc +26 -0
  6. package/.mcp.json +4 -4
  7. package/CHANGELOG.md +14 -0
  8. package/CODE_OF_CONDUCT.md +27 -0
  9. package/CONTRIBUTING.md +62 -0
  10. package/OMG-setup.sh +1201 -355
  11. package/README.md +77 -56
  12. package/SECURITY.md +25 -0
  13. package/agents/__init__.py +1 -0
  14. package/agents/model_roles.py +196 -0
  15. package/agents/omg-architect-mode.md +3 -5
  16. package/agents/omg-backend-engineer.md +3 -5
  17. package/agents/omg-database-engineer.md +3 -5
  18. package/agents/omg-frontend-designer.md +4 -5
  19. package/agents/omg-implement-mode.md +4 -5
  20. package/agents/omg-infra-engineer.md +3 -5
  21. package/agents/omg-research-mode.md +4 -6
  22. package/agents/omg-security-auditor.md +3 -5
  23. package/agents/omg-testing-engineer.md +3 -5
  24. package/build/lib/yaml.py +321 -0
  25. package/commands/OMG:ai-commit.md +101 -14
  26. package/commands/OMG:arch.md +302 -19
  27. package/commands/OMG:ccg.md +12 -7
  28. package/commands/OMG:compat.md +25 -17
  29. package/commands/OMG:cost.md +173 -13
  30. package/commands/OMG:crazy.md +1 -1
  31. package/commands/OMG:create-agent.md +170 -20
  32. package/commands/OMG:deps.md +235 -17
  33. package/commands/OMG:domain-init.md +1 -1
  34. package/commands/OMG:escalate.md +41 -12
  35. package/commands/OMG:health-check.md +37 -13
  36. package/commands/OMG:init.md +122 -14
  37. package/commands/OMG:project-init.md +1 -1
  38. package/commands/OMG:session-branch.md +76 -9
  39. package/commands/OMG:session-fork.md +42 -5
  40. package/commands/OMG:session-merge.md +124 -8
  41. package/commands/OMG:setup.md +69 -12
  42. package/commands/OMG:stats.md +215 -14
  43. package/commands/OMG:teams.md +19 -10
  44. package/config/lsp_languages.yaml +8 -0
  45. package/hooks/__init__.py +0 -0
  46. package/hooks/_agent_registry.py +423 -0
  47. package/hooks/_analytics.py +291 -0
  48. package/hooks/_budget.py +31 -0
  49. package/hooks/_common.py +569 -0
  50. package/hooks/_compression_optimizer.py +119 -0
  51. package/hooks/_cost_ledger.py +176 -0
  52. package/hooks/_learnings.py +126 -0
  53. package/hooks/_memory.py +103 -0
  54. package/hooks/_protected_context.py +150 -0
  55. package/hooks/_token_counter.py +221 -0
  56. package/hooks/branch_manager.py +236 -0
  57. package/hooks/budget_governor.py +232 -0
  58. package/hooks/circuit-breaker.py +270 -0
  59. package/hooks/compression_feedback.py +254 -0
  60. package/hooks/config-guard.py +216 -0
  61. package/hooks/context_pressure.py +53 -0
  62. package/hooks/credential_store.py +1020 -0
  63. package/hooks/fetch-rate-limits.py +212 -0
  64. package/hooks/firewall.py +48 -0
  65. package/hooks/hashline-formatter-bridge.py +224 -0
  66. package/hooks/hashline-injector.py +273 -0
  67. package/hooks/hashline-validator.py +216 -0
  68. package/hooks/idle-detector.py +95 -0
  69. package/hooks/intentgate-keyword-detector.py +188 -0
  70. package/hooks/magic-keyword-router.py +195 -0
  71. package/hooks/policy_engine.py +505 -0
  72. package/hooks/post-tool-failure.py +19 -0
  73. package/hooks/post-write.py +219 -0
  74. package/hooks/post_write.py +46 -0
  75. package/hooks/pre-compact.py +398 -0
  76. package/hooks/pre-tool-inject.py +98 -0
  77. package/hooks/prompt-enhancer.py +672 -0
  78. package/hooks/quality-runner.py +191 -0
  79. package/hooks/query.py +512 -0
  80. package/hooks/secret-guard.py +61 -0
  81. package/hooks/secret_audit.py +144 -0
  82. package/hooks/session-end-capture.py +137 -0
  83. package/hooks/session-start.py +277 -0
  84. package/hooks/setup_wizard.py +582 -0
  85. package/hooks/shadow_manager.py +297 -0
  86. package/hooks/state_migration.py +225 -0
  87. package/hooks/stop-gate.py +7 -0
  88. package/hooks/stop_dispatcher.py +945 -0
  89. package/hooks/test-validator.py +361 -0
  90. package/hooks/test_generator_hook.py +123 -0
  91. package/hooks/todo-state-tracker.py +114 -0
  92. package/hooks/tool-ledger.py +149 -0
  93. package/hooks/trust_review.py +585 -0
  94. package/hud/omg-hud.mjs +31 -1
  95. package/lab/__init__.py +1 -0
  96. package/lab/pipeline.py +75 -0
  97. package/lab/policies.py +52 -0
  98. package/package.json +7 -18
  99. package/plugins/README.md +33 -61
  100. package/plugins/advanced/commands/OMG:deep-plan.md +3 -3
  101. package/plugins/advanced/commands/OMG:learn.md +1 -1
  102. package/plugins/advanced/commands/OMG:security-review.md +3 -3
  103. package/plugins/advanced/commands/OMG:ship.md +1 -1
  104. package/plugins/advanced/plugin.json +1 -1
  105. package/plugins/core/plugin.json +8 -3
  106. package/plugins/dephealth/__init__.py +0 -0
  107. package/plugins/dephealth/cve_scanner.py +188 -0
  108. package/plugins/dephealth/license_checker.py +135 -0
  109. package/plugins/dephealth/manifest_detector.py +423 -0
  110. package/plugins/dephealth/vuln_analyzer.py +169 -0
  111. package/plugins/testgen/__init__.py +0 -0
  112. package/plugins/testgen/codamosa_engine.py +402 -0
  113. package/plugins/testgen/edge_case_synthesizer.py +184 -0
  114. package/plugins/testgen/framework_detector.py +271 -0
  115. package/plugins/testgen/skeleton_generator.py +219 -0
  116. package/plugins/viz/__init__.py +0 -0
  117. package/plugins/viz/ast_parser.py +139 -0
  118. package/plugins/viz/diagram_generator.py +192 -0
  119. package/plugins/viz/graph_builder.py +444 -0
  120. package/plugins/viz/native_parsers.py +259 -0
  121. package/plugins/viz/regex_parser.py +112 -0
  122. package/pyproject.toml +81 -0
  123. package/rules/contextual/write-verify.md +2 -2
  124. package/rules/core/00-truth.md +1 -1
  125. package/rules/core/01-surgical.md +1 -1
  126. package/rules/core/02-circuit-breaker.md +2 -2
  127. package/rules/core/03-ensemble.md +3 -3
  128. package/rules/core/04-testing.md +3 -3
  129. package/runtime/__init__.py +32 -0
  130. package/runtime/adapters/__init__.py +13 -0
  131. package/runtime/adapters/claude.py +60 -0
  132. package/runtime/adapters/gpt.py +53 -0
  133. package/runtime/adapters/local.py +53 -0
  134. package/runtime/adoption.py +212 -0
  135. package/runtime/business_workflow.py +220 -0
  136. package/runtime/cli_provider.py +85 -0
  137. package/runtime/compat.py +1299 -0
  138. package/runtime/custom_agent_loader.py +366 -0
  139. package/runtime/dispatcher.py +47 -0
  140. package/runtime/ecosystem.py +371 -0
  141. package/runtime/legacy_compat.py +7 -0
  142. package/runtime/mcp_config_writers.py +115 -0
  143. package/runtime/mcp_lifecycle.py +153 -0
  144. package/runtime/mcp_memory_server.py +135 -0
  145. package/runtime/memory_parsers/__init__.py +0 -0
  146. package/runtime/memory_parsers/chatgpt_parser.py +257 -0
  147. package/runtime/memory_parsers/claude_import.py +107 -0
  148. package/runtime/memory_parsers/export.py +97 -0
  149. package/runtime/memory_parsers/gemini_import.py +91 -0
  150. package/runtime/memory_parsers/kimi_import.py +91 -0
  151. package/runtime/memory_store.py +215 -0
  152. package/runtime/omc_compat.py +7 -0
  153. package/runtime/providers/__init__.py +0 -0
  154. package/runtime/providers/codex_provider.py +112 -0
  155. package/runtime/providers/gemini_provider.py +128 -0
  156. package/runtime/providers/kimi_provider.py +151 -0
  157. package/runtime/providers/opencode_provider.py +144 -0
  158. package/runtime/subagent_dispatcher.py +362 -0
  159. package/runtime/team_router.py +1167 -0
  160. package/runtime/tmux_session_manager.py +169 -0
  161. package/scripts/check-omg-compat-contract-snapshot.py +137 -0
  162. package/scripts/check-omg-contract-snapshot.py +12 -0
  163. package/scripts/check-omg-public-ready.py +193 -0
  164. package/scripts/check-omg-standalone-clean.py +103 -0
  165. package/scripts/legacy_to_omg_migrate.py +29 -0
  166. package/scripts/migrate-legacy.py +464 -0
  167. package/scripts/omc_to_omg_migrate.py +12 -0
  168. package/scripts/omg.py +492 -0
  169. package/scripts/settings-merge.py +283 -0
  170. package/scripts/verify-standalone.sh +8 -4
  171. package/settings.json +126 -29
  172. package/templates/profile.yaml +1 -1
  173. package/tools/__init__.py +2 -0
  174. package/tools/browser_consent.py +289 -0
  175. package/tools/browser_stealth.py +481 -0
  176. package/tools/browser_tool.py +448 -0
  177. package/tools/changelog_generator.py +347 -0
  178. package/tools/commit_splitter.py +746 -0
  179. package/tools/config_discovery.py +151 -0
  180. package/tools/config_merger.py +449 -0
  181. package/tools/dashboard_generator.py +300 -0
  182. package/tools/git_inspector.py +298 -0
  183. package/tools/lsp_client.py +275 -0
  184. package/tools/lsp_discovery.py +231 -0
  185. package/tools/lsp_operations.py +392 -0
  186. package/tools/pr_generator.py +404 -0
  187. package/tools/python_repl.py +656 -0
  188. package/tools/python_sandbox.py +609 -0
  189. package/tools/search_providers/__init__.py +77 -0
  190. package/tools/search_providers/brave.py +115 -0
  191. package/tools/search_providers/exa.py +116 -0
  192. package/tools/search_providers/jina.py +104 -0
  193. package/tools/search_providers/perplexity.py +139 -0
  194. package/tools/search_providers/synthetic.py +74 -0
  195. package/tools/session_snapshot.py +736 -0
  196. package/tools/ssh_manager.py +912 -0
  197. package/tools/theme_engine.py +294 -0
  198. package/tools/theme_selector.py +137 -0
  199. package/tools/web_search.py +622 -0
  200. package/yaml.py +321 -0
  201. package/.claude-plugin/scripts/install.sh +0 -9
  202. package/bun.lock +0 -23
  203. package/bunfig.toml +0 -3
  204. package/hooks/_budget.ts +0 -1
  205. package/hooks/_common.ts +0 -63
  206. package/hooks/circuit-breaker.ts +0 -101
  207. package/hooks/config-guard.ts +0 -4
  208. package/hooks/firewall.ts +0 -20
  209. package/hooks/policy_engine.ts +0 -156
  210. package/hooks/post-tool-failure.ts +0 -22
  211. package/hooks/post-write.ts +0 -4
  212. package/hooks/pre-tool-inject.ts +0 -4
  213. package/hooks/prompt-enhancer.ts +0 -46
  214. package/hooks/quality-runner.ts +0 -24
  215. package/hooks/secret-guard.ts +0 -4
  216. package/hooks/session-end-capture.ts +0 -19
  217. package/hooks/session-start.ts +0 -19
  218. package/hooks/shadow_manager.ts +0 -81
  219. package/hooks/stop-gate.ts +0 -22
  220. package/hooks/stop_dispatcher.ts +0 -147
  221. package/hooks/test-generator-hook.ts +0 -4
  222. package/hooks/tool-ledger.ts +0 -27
  223. package/hooks/trust_review.ts +0 -175
  224. package/lab/pipeline.ts +0 -75
  225. package/lab/policies.ts +0 -68
  226. package/runtime/common.ts +0 -111
  227. package/runtime/compat.ts +0 -174
  228. package/runtime/dispatcher.ts +0 -25
  229. package/runtime/ecosystem.ts +0 -186
  230. package/runtime/provider_bootstrap.ts +0 -99
  231. package/runtime/provider_smoke.ts +0 -34
  232. package/runtime/release_readiness.ts +0 -186
  233. package/runtime/team_router.ts +0 -144
  234. package/scripts/check-omg-compat-contract-snapshot.ts +0 -20
  235. package/scripts/check-omg-standalone-clean.ts +0 -12
  236. package/scripts/check-runtime-clean.ts +0 -94
  237. package/scripts/omg.ts +0 -352
  238. package/scripts/settings-merge.ts +0 -93
  239. package/tools/commit_splitter.ts +0 -23
  240. package/tools/git_inspector.ts +0 -18
  241. package/tools/session_snapshot.ts +0 -47
  242. package/trac3er-oh-my-god-2.0.0.tgz +0 -0
  243. package/tsconfig.json +0 -15
@@ -0,0 +1,366 @@
1
+ """Custom Agent Loader — scans user/project agent dirs, hot-reloads on change.
2
+
3
+ Scans ~/.omg/agents/ (user-level) and <project>/.omg/agents/ (project-level)
4
+ for custom agent markdown files. Project-level agents override user-level.
5
+
6
+ Feature flag: OMG_CUSTOM_AGENTS_ENABLED (default: False)
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import re
12
+ import sys
13
+ import time
14
+ from pathlib import Path
15
+ from typing import Any, Callable
16
+
17
+ # --- Lazy import helpers ---
18
+ _OMG_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
19
+
20
+
21
+ def _get_feature_flag() -> Any:
22
+ """Lazy-import get_feature_flag from hooks/_common.py."""
23
+ hooks_dir = os.path.join(_OMG_ROOT, "hooks")
24
+ if hooks_dir not in sys.path:
25
+ sys.path.insert(0, hooks_dir)
26
+ try:
27
+ from _common import get_feature_flag # pyright: ignore[reportMissingImports]
28
+ return get_feature_flag
29
+ except ImportError:
30
+ return None
31
+
32
+
33
+ def _is_enabled() -> bool:
34
+ """Check if custom agents feature is enabled.
35
+
36
+ Resolution: env var OMG_CUSTOM_AGENTS_ENABLED → settings.json → default False.
37
+ """
38
+ # Fast path: check env var directly
39
+ env_val = os.environ.get("OMG_CUSTOM_AGENTS_ENABLED", "").lower()
40
+ if env_val in ("0", "false", "no"):
41
+ return False
42
+ if env_val in ("1", "true", "yes"):
43
+ return True
44
+
45
+ # Slow path: check via get_feature_flag
46
+ get_flag = _get_feature_flag()
47
+ if get_flag is not None:
48
+ return get_flag("CUSTOM_AGENTS", default=False)
49
+ return False
50
+
51
+
52
+ # --- Schema validation ---
53
+
54
+ # Required sections
55
+ _AGENT_HEADER_RE = re.compile(r"^#\s+Agent:\s*.+", re.MULTILINE)
56
+ _ROLE_SECTION_RE = re.compile(r"^##\s+Role\b", re.MULTILINE)
57
+
58
+ # Optional sections (validated if present)
59
+ _OPTIONAL_SECTIONS = {
60
+ "Model": re.compile(r"^##\s+Model\b", re.MULTILINE),
61
+ "Capabilities": re.compile(r"^##\s+Capabilities\b", re.MULTILINE),
62
+ "Instructions": re.compile(r"^##\s+Instructions\b", re.MULTILINE),
63
+ }
64
+
65
+
66
+ def _validate_agent_schema(content: str) -> tuple[bool, list[str]]:
67
+ """Validate custom agent markdown schema.
68
+
69
+ Args:
70
+ content: Markdown content of the agent file.
71
+
72
+ Returns:
73
+ (is_valid, issues) where issues is a list of validation error strings.
74
+ Required: ``# Agent:`` header AND ``## Role`` section.
75
+ Optional but validated if present: ``## Model``, ``## Capabilities``, ``## Instructions``.
76
+ """
77
+ issues: list[str] = []
78
+
79
+ if not content or not content.strip():
80
+ return False, ["Agent file is empty"]
81
+
82
+ # Required: # Agent: header
83
+ if not _AGENT_HEADER_RE.search(content):
84
+ issues.append("Missing required '# Agent: <name>' header")
85
+
86
+ # Required: ## Role section
87
+ if not _ROLE_SECTION_RE.search(content):
88
+ issues.append("Missing required '## Role' section")
89
+
90
+ # Optional sections: validate format if present (check for typos)
91
+ # We don't flag missing optional sections, but if they're present with wrong format,
92
+ # we note it as informational (not blocking).
93
+
94
+ is_valid = len(issues) == 0
95
+ return is_valid, issues
96
+
97
+
98
+ def _extract_agent_name(content: str, filename: str) -> str:
99
+ """Extract agent name from # Agent: header or fall back to filename."""
100
+ match = _AGENT_HEADER_RE.search(content)
101
+ if match:
102
+ # Extract the name part after "# Agent:"
103
+ line = match.group(0)
104
+ name = line.split(":", 1)[1].strip() if ":" in line else ""
105
+ if name:
106
+ return name.lower().replace(" ", "_")
107
+ # Fallback: use filename without extension
108
+ return Path(filename).stem.lower().replace("-", "_")
109
+
110
+
111
+ def _extract_description(content: str) -> str:
112
+ """Extract description from ## Role section content."""
113
+ match = _ROLE_SECTION_RE.search(content)
114
+ if not match:
115
+ return ""
116
+ # Get text after ## Role until next ## or end
117
+ rest = content[match.end():]
118
+ next_section = re.search(r"^##\s+", rest, re.MULTILINE)
119
+ if next_section:
120
+ rest = rest[:next_section.start()]
121
+ # Take first non-empty line as description
122
+ for line in rest.strip().splitlines():
123
+ line = line.strip()
124
+ if line:
125
+ return line[:200] # Cap at 200 chars
126
+ return ""
127
+
128
+
129
+ def _extract_model_role(content: str) -> str | None:
130
+ """Extract model role from ## Model section if present."""
131
+ model_match = _OPTIONAL_SECTIONS["Model"].search(content)
132
+ if not model_match:
133
+ return None
134
+ rest = content[model_match.end():]
135
+ next_section = re.search(r"^##\s+", rest, re.MULTILINE)
136
+ if next_section:
137
+ rest = rest[:next_section.start()]
138
+ text = rest.strip().lower()
139
+ # Look for known role keywords
140
+ for role in ("smol", "slow", "default", "fast"):
141
+ if role in text:
142
+ return role
143
+ return None
144
+
145
+
146
+ # --- Agent scanning ---
147
+
148
+ def _get_user_agents_dir() -> str:
149
+ """Get user-level agents directory: ~/.omg/agents/"""
150
+ return os.path.join(os.path.expanduser("~"), ".omg", "agents")
151
+
152
+
153
+ def _get_project_agents_dir(project_dir: str) -> str:
154
+ """Get project-level agents directory: <project>/.omg/agents/"""
155
+ return os.path.join(project_dir, ".omg", "agents")
156
+
157
+
158
+ def _scan_agents_dir(agents_dir: str, level: str) -> list[dict[str, Any]]:
159
+ """Scan a directory for agent .md files.
160
+
161
+ Args:
162
+ agents_dir: Path to scan for .md files.
163
+ level: 'user' or 'project'.
164
+
165
+ Returns:
166
+ List of agent info dicts.
167
+ """
168
+ agents: list[dict[str, Any]] = []
169
+
170
+ if not os.path.isdir(agents_dir):
171
+ return agents
172
+
173
+ for filename in sorted(os.listdir(agents_dir)):
174
+ if not filename.endswith(".md"):
175
+ continue
176
+
177
+ filepath = os.path.join(agents_dir, filename)
178
+ if not os.path.isfile(filepath):
179
+ continue
180
+
181
+ try:
182
+ with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
183
+ content = f.read(256 * 1024) # 256KB limit
184
+ except OSError:
185
+ continue
186
+
187
+ is_valid, issues = _validate_agent_schema(content)
188
+ name = _extract_agent_name(content, filename)
189
+ description = _extract_description(content)
190
+ model_role = _extract_model_role(content)
191
+
192
+ agents.append({
193
+ "name": name,
194
+ "file": filepath,
195
+ "level": level,
196
+ "model_role": model_role,
197
+ "description": description,
198
+ "validated": is_valid,
199
+ "issues": issues,
200
+ })
201
+
202
+ return agents
203
+
204
+
205
+ def load_custom_agents(project_dir: str = ".") -> list[dict[str, Any]]:
206
+ """Load custom agents from user and project directories.
207
+
208
+ If OMG_CUSTOM_AGENTS_ENABLED is False, returns empty list.
209
+ Scans ~/.omg/agents/*.md (user-level) and <project_dir>/.omg/agents/*.md (project-level).
210
+ Project-level agents override user-level agents with the same name.
211
+
212
+ Args:
213
+ project_dir: Project directory (default: current directory).
214
+
215
+ Returns:
216
+ List of agent info dicts with keys:
217
+ name, file, level, model_role, description, validated, issues
218
+ """
219
+ if not _is_enabled():
220
+ return []
221
+
222
+ project_dir = os.path.abspath(project_dir)
223
+
224
+ # Scan user-level first
225
+ user_dir = _get_user_agents_dir()
226
+ user_agents = _scan_agents_dir(user_dir, "user")
227
+
228
+ # Scan project-level
229
+ project_agents_dir = _get_project_agents_dir(project_dir)
230
+ project_agents = _scan_agents_dir(project_agents_dir, "project")
231
+
232
+ # Merge: project overrides user with same name
233
+ agents_by_name: dict[str, dict[str, Any]] = {}
234
+ for agent in user_agents:
235
+ agents_by_name[agent["name"]] = agent
236
+ for agent in project_agents:
237
+ agents_by_name[agent["name"]] = agent # Override user-level
238
+
239
+ return list(agents_by_name.values())
240
+
241
+
242
+ # --- Hot-reload watcher ---
243
+
244
+ def _get_dir_state(agents_dir: str) -> dict[str, float]:
245
+ """Get mtime state of all .md files in a directory.
246
+
247
+ Returns:
248
+ Dict mapping filename → mtime.
249
+ """
250
+ state: dict[str, float] = {}
251
+ if not os.path.isdir(agents_dir):
252
+ return state
253
+ try:
254
+ for filename in os.listdir(agents_dir):
255
+ if not filename.endswith(".md"):
256
+ continue
257
+ filepath = os.path.join(agents_dir, filename)
258
+ try:
259
+ state[filepath] = os.stat(filepath).st_mtime
260
+ except OSError:
261
+ continue
262
+ except OSError:
263
+ pass
264
+ return state
265
+
266
+
267
+ def watch_for_changes(
268
+ project_dir: str,
269
+ callback: Callable[[list[dict[str, Any]]], None],
270
+ poll_interval: float = 5.0,
271
+ max_iterations: int | None = None,
272
+ ) -> None:
273
+ """Poll agent dirs for changes and call callback when detected.
274
+
275
+ Uses stdlib-only mtime polling (no watchdog dependency).
276
+ Polls every ``poll_interval`` seconds.
277
+
278
+ Args:
279
+ project_dir: Project directory.
280
+ callback: Called with updated agent list when changes detected.
281
+ poll_interval: Seconds between polls (default: 5.0).
282
+ max_iterations: If set, stop after this many iterations (for testing).
283
+ """
284
+ project_dir = os.path.abspath(project_dir)
285
+ user_dir = _get_user_agents_dir()
286
+ project_agents_dir = _get_project_agents_dir(project_dir)
287
+
288
+ # Initial state
289
+ prev_user_state = _get_dir_state(user_dir)
290
+ prev_project_state = _get_dir_state(project_agents_dir)
291
+
292
+ iterations = 0
293
+ while True:
294
+ if max_iterations is not None and iterations >= max_iterations:
295
+ break
296
+
297
+ time.sleep(poll_interval)
298
+ iterations += 1
299
+
300
+ # Check for changes
301
+ curr_user_state = _get_dir_state(user_dir)
302
+ curr_project_state = _get_dir_state(project_agents_dir)
303
+
304
+ if curr_user_state != prev_user_state or curr_project_state != prev_project_state:
305
+ # Reload agents
306
+ agents = load_custom_agents(project_dir)
307
+ callback(agents)
308
+ prev_user_state = curr_user_state
309
+ prev_project_state = curr_project_state
310
+
311
+
312
+ # --- Merged agent registry ---
313
+
314
+ def get_all_agents(project_dir: str = ".") -> dict[str, dict[str, Any]]:
315
+ """Return merged dict of built-in + custom agents.
316
+
317
+ Custom agents extend/override built-in registry entries.
318
+ Returns dict mapping agent name → agent config.
319
+
320
+ Args:
321
+ project_dir: Project directory (default: current directory).
322
+
323
+ Returns:
324
+ Dict of agent configs. Built-in agents have standard keys.
325
+ Custom agents have: name, file, level, model_role, description,
326
+ validated, preferred_model, task_category, trigger_keywords, etc.
327
+ """
328
+ # Get built-in agents via lazy import
329
+ hooks_dir = os.path.join(_OMG_ROOT, "hooks")
330
+ if hooks_dir not in sys.path:
331
+ sys.path.insert(0, hooks_dir)
332
+
333
+ merged: dict[str, dict[str, Any]] = {}
334
+
335
+ try:
336
+ from _agent_registry import AGENT_REGISTRY # pyright: ignore[reportMissingImports]
337
+ # Copy built-in agents
338
+ for name, config in AGENT_REGISTRY.items():
339
+ merged[name] = dict(config)
340
+ merged[name]["source"] = "builtin"
341
+ except ImportError:
342
+ pass
343
+
344
+ # Overlay custom agents
345
+ custom_agents = load_custom_agents(project_dir)
346
+ for agent in custom_agents:
347
+ if not agent.get("validated", False):
348
+ continue # Skip invalid custom agents
349
+
350
+ name = agent["name"]
351
+ merged[name] = {
352
+ "preferred_model": "claude",
353
+ "task_category": "unspecified-high",
354
+ "skills": [],
355
+ "trigger_keywords": set(),
356
+ "mcp_tools": [],
357
+ "description": agent.get("description", ""),
358
+ "agent_file": agent.get("file", ""),
359
+ "model_version": "claude-sonnet-4-5",
360
+ "model_role": agent.get("model_role"),
361
+ "source": "custom",
362
+ "level": agent.get("level", "unknown"),
363
+ "validated": True,
364
+ }
365
+
366
+ return merged
@@ -0,0 +1,47 @@
1
+ """Runtime dispatch orchestration for OMG v1 adapters."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from runtime.adapters import get_adapters
7
+ from runtime.business_workflow import build_business_workflow_result
8
+
9
+
10
+ def dispatch_runtime(runtime: str, idea: dict[str, Any]) -> dict[str, Any]:
11
+ adapters = get_adapters()
12
+ adapter = adapters.get(runtime)
13
+ if adapter is None:
14
+ return {
15
+ "status": "error",
16
+ "error_code": "RUNTIME_NOT_FOUND",
17
+ "runtime": runtime,
18
+ "message": f"Unknown runtime: {runtime}",
19
+ }
20
+
21
+ try:
22
+ plan = adapter.plan(idea)
23
+ execution = adapter.execute(plan)
24
+ verification = adapter.verify(execution)
25
+ evidence = adapter.collect_evidence(verification)
26
+ business_workflow = build_business_workflow_result(
27
+ idea=idea,
28
+ plan=plan,
29
+ execution=execution,
30
+ verification=verification,
31
+ )
32
+ return {
33
+ "status": "ok",
34
+ "runtime": runtime,
35
+ "plan": plan,
36
+ "execution": execution,
37
+ "verification": verification,
38
+ "evidence": evidence,
39
+ "business_workflow": business_workflow,
40
+ }
41
+ except Exception as exc: # pragma: no cover - defensive guard
42
+ return {
43
+ "status": "error",
44
+ "error_code": "RUNTIME_EXECUTION_FAILED",
45
+ "runtime": runtime,
46
+ "message": str(exc),
47
+ }