@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.
- package/.claude-plugin/marketplace.json +8 -8
- package/.claude-plugin/plugin.json +5 -4
- package/.claude-plugin/scripts/uninstall.sh +74 -3
- package/.claude-plugin/scripts/update.sh +78 -3
- package/.coveragerc +26 -0
- package/.mcp.json +4 -4
- package/CHANGELOG.md +14 -0
- package/CODE_OF_CONDUCT.md +27 -0
- package/CONTRIBUTING.md +62 -0
- package/OMG-setup.sh +1201 -355
- package/README.md +77 -56
- package/SECURITY.md +25 -0
- package/agents/__init__.py +1 -0
- package/agents/model_roles.py +196 -0
- package/agents/omg-architect-mode.md +3 -5
- package/agents/omg-backend-engineer.md +3 -5
- package/agents/omg-database-engineer.md +3 -5
- package/agents/omg-frontend-designer.md +4 -5
- package/agents/omg-implement-mode.md +4 -5
- package/agents/omg-infra-engineer.md +3 -5
- package/agents/omg-research-mode.md +4 -6
- package/agents/omg-security-auditor.md +3 -5
- package/agents/omg-testing-engineer.md +3 -5
- package/build/lib/yaml.py +321 -0
- package/commands/OMG:ai-commit.md +101 -14
- package/commands/OMG:arch.md +302 -19
- package/commands/OMG:ccg.md +12 -7
- package/commands/OMG:compat.md +25 -17
- package/commands/OMG:cost.md +173 -13
- package/commands/OMG:crazy.md +1 -1
- package/commands/OMG:create-agent.md +170 -20
- package/commands/OMG:deps.md +235 -17
- package/commands/OMG:domain-init.md +1 -1
- package/commands/OMG:escalate.md +41 -12
- package/commands/OMG:health-check.md +37 -13
- package/commands/OMG:init.md +122 -14
- package/commands/OMG:project-init.md +1 -1
- package/commands/OMG:session-branch.md +76 -9
- package/commands/OMG:session-fork.md +42 -5
- package/commands/OMG:session-merge.md +124 -8
- package/commands/OMG:setup.md +69 -12
- package/commands/OMG:stats.md +215 -14
- package/commands/OMG:teams.md +19 -10
- package/config/lsp_languages.yaml +8 -0
- package/hooks/__init__.py +0 -0
- package/hooks/_agent_registry.py +423 -0
- package/hooks/_analytics.py +291 -0
- package/hooks/_budget.py +31 -0
- package/hooks/_common.py +569 -0
- package/hooks/_compression_optimizer.py +119 -0
- package/hooks/_cost_ledger.py +176 -0
- package/hooks/_learnings.py +126 -0
- package/hooks/_memory.py +103 -0
- package/hooks/_protected_context.py +150 -0
- package/hooks/_token_counter.py +221 -0
- package/hooks/branch_manager.py +236 -0
- package/hooks/budget_governor.py +232 -0
- package/hooks/circuit-breaker.py +270 -0
- package/hooks/compression_feedback.py +254 -0
- package/hooks/config-guard.py +216 -0
- package/hooks/context_pressure.py +53 -0
- package/hooks/credential_store.py +1020 -0
- package/hooks/fetch-rate-limits.py +212 -0
- package/hooks/firewall.py +48 -0
- package/hooks/hashline-formatter-bridge.py +224 -0
- package/hooks/hashline-injector.py +273 -0
- package/hooks/hashline-validator.py +216 -0
- package/hooks/idle-detector.py +95 -0
- package/hooks/intentgate-keyword-detector.py +188 -0
- package/hooks/magic-keyword-router.py +195 -0
- package/hooks/policy_engine.py +505 -0
- package/hooks/post-tool-failure.py +19 -0
- package/hooks/post-write.py +219 -0
- package/hooks/post_write.py +46 -0
- package/hooks/pre-compact.py +398 -0
- package/hooks/pre-tool-inject.py +98 -0
- package/hooks/prompt-enhancer.py +672 -0
- package/hooks/quality-runner.py +191 -0
- package/hooks/query.py +512 -0
- package/hooks/secret-guard.py +61 -0
- package/hooks/secret_audit.py +144 -0
- package/hooks/session-end-capture.py +137 -0
- package/hooks/session-start.py +277 -0
- package/hooks/setup_wizard.py +582 -0
- package/hooks/shadow_manager.py +297 -0
- package/hooks/state_migration.py +225 -0
- package/hooks/stop-gate.py +7 -0
- package/hooks/stop_dispatcher.py +945 -0
- package/hooks/test-validator.py +361 -0
- package/hooks/test_generator_hook.py +123 -0
- package/hooks/todo-state-tracker.py +114 -0
- package/hooks/tool-ledger.py +149 -0
- package/hooks/trust_review.py +585 -0
- package/hud/omg-hud.mjs +31 -1
- package/lab/__init__.py +1 -0
- package/lab/pipeline.py +75 -0
- package/lab/policies.py +52 -0
- package/package.json +7 -18
- package/plugins/README.md +33 -61
- package/plugins/advanced/commands/OMG:deep-plan.md +3 -3
- package/plugins/advanced/commands/OMG:learn.md +1 -1
- package/plugins/advanced/commands/OMG:security-review.md +3 -3
- package/plugins/advanced/commands/OMG:ship.md +1 -1
- package/plugins/advanced/plugin.json +1 -1
- package/plugins/core/plugin.json +8 -3
- package/plugins/dephealth/__init__.py +0 -0
- package/plugins/dephealth/cve_scanner.py +188 -0
- package/plugins/dephealth/license_checker.py +135 -0
- package/plugins/dephealth/manifest_detector.py +423 -0
- package/plugins/dephealth/vuln_analyzer.py +169 -0
- package/plugins/testgen/__init__.py +0 -0
- package/plugins/testgen/codamosa_engine.py +402 -0
- package/plugins/testgen/edge_case_synthesizer.py +184 -0
- package/plugins/testgen/framework_detector.py +271 -0
- package/plugins/testgen/skeleton_generator.py +219 -0
- package/plugins/viz/__init__.py +0 -0
- package/plugins/viz/ast_parser.py +139 -0
- package/plugins/viz/diagram_generator.py +192 -0
- package/plugins/viz/graph_builder.py +444 -0
- package/plugins/viz/native_parsers.py +259 -0
- package/plugins/viz/regex_parser.py +112 -0
- package/pyproject.toml +81 -0
- package/rules/contextual/write-verify.md +2 -2
- package/rules/core/00-truth.md +1 -1
- package/rules/core/01-surgical.md +1 -1
- package/rules/core/02-circuit-breaker.md +2 -2
- package/rules/core/03-ensemble.md +3 -3
- package/rules/core/04-testing.md +3 -3
- package/runtime/__init__.py +32 -0
- package/runtime/adapters/__init__.py +13 -0
- package/runtime/adapters/claude.py +60 -0
- package/runtime/adapters/gpt.py +53 -0
- package/runtime/adapters/local.py +53 -0
- package/runtime/adoption.py +212 -0
- package/runtime/business_workflow.py +220 -0
- package/runtime/cli_provider.py +85 -0
- package/runtime/compat.py +1299 -0
- package/runtime/custom_agent_loader.py +366 -0
- package/runtime/dispatcher.py +47 -0
- package/runtime/ecosystem.py +371 -0
- package/runtime/legacy_compat.py +7 -0
- package/runtime/mcp_config_writers.py +115 -0
- package/runtime/mcp_lifecycle.py +153 -0
- package/runtime/mcp_memory_server.py +135 -0
- package/runtime/memory_parsers/__init__.py +0 -0
- package/runtime/memory_parsers/chatgpt_parser.py +257 -0
- package/runtime/memory_parsers/claude_import.py +107 -0
- package/runtime/memory_parsers/export.py +97 -0
- package/runtime/memory_parsers/gemini_import.py +91 -0
- package/runtime/memory_parsers/kimi_import.py +91 -0
- package/runtime/memory_store.py +215 -0
- package/runtime/omc_compat.py +7 -0
- package/runtime/providers/__init__.py +0 -0
- package/runtime/providers/codex_provider.py +112 -0
- package/runtime/providers/gemini_provider.py +128 -0
- package/runtime/providers/kimi_provider.py +151 -0
- package/runtime/providers/opencode_provider.py +144 -0
- package/runtime/subagent_dispatcher.py +362 -0
- package/runtime/team_router.py +1167 -0
- package/runtime/tmux_session_manager.py +169 -0
- package/scripts/check-omg-compat-contract-snapshot.py +137 -0
- package/scripts/check-omg-contract-snapshot.py +12 -0
- package/scripts/check-omg-public-ready.py +193 -0
- package/scripts/check-omg-standalone-clean.py +103 -0
- package/scripts/legacy_to_omg_migrate.py +29 -0
- package/scripts/migrate-legacy.py +464 -0
- package/scripts/omc_to_omg_migrate.py +12 -0
- package/scripts/omg.py +492 -0
- package/scripts/settings-merge.py +283 -0
- package/scripts/verify-standalone.sh +8 -4
- package/settings.json +126 -29
- package/templates/profile.yaml +1 -1
- package/tools/__init__.py +2 -0
- package/tools/browser_consent.py +289 -0
- package/tools/browser_stealth.py +481 -0
- package/tools/browser_tool.py +448 -0
- package/tools/changelog_generator.py +347 -0
- package/tools/commit_splitter.py +746 -0
- package/tools/config_discovery.py +151 -0
- package/tools/config_merger.py +449 -0
- package/tools/dashboard_generator.py +300 -0
- package/tools/git_inspector.py +298 -0
- package/tools/lsp_client.py +275 -0
- package/tools/lsp_discovery.py +231 -0
- package/tools/lsp_operations.py +392 -0
- package/tools/pr_generator.py +404 -0
- package/tools/python_repl.py +656 -0
- package/tools/python_sandbox.py +609 -0
- package/tools/search_providers/__init__.py +77 -0
- package/tools/search_providers/brave.py +115 -0
- package/tools/search_providers/exa.py +116 -0
- package/tools/search_providers/jina.py +104 -0
- package/tools/search_providers/perplexity.py +139 -0
- package/tools/search_providers/synthetic.py +74 -0
- package/tools/session_snapshot.py +736 -0
- package/tools/ssh_manager.py +912 -0
- package/tools/theme_engine.py +294 -0
- package/tools/theme_selector.py +137 -0
- package/tools/web_search.py +622 -0
- package/yaml.py +321 -0
- package/.claude-plugin/scripts/install.sh +0 -9
- package/bun.lock +0 -23
- package/bunfig.toml +0 -3
- package/hooks/_budget.ts +0 -1
- package/hooks/_common.ts +0 -63
- package/hooks/circuit-breaker.ts +0 -101
- package/hooks/config-guard.ts +0 -4
- package/hooks/firewall.ts +0 -20
- package/hooks/policy_engine.ts +0 -156
- package/hooks/post-tool-failure.ts +0 -22
- package/hooks/post-write.ts +0 -4
- package/hooks/pre-tool-inject.ts +0 -4
- package/hooks/prompt-enhancer.ts +0 -46
- package/hooks/quality-runner.ts +0 -24
- package/hooks/secret-guard.ts +0 -4
- package/hooks/session-end-capture.ts +0 -19
- package/hooks/session-start.ts +0 -19
- package/hooks/shadow_manager.ts +0 -81
- package/hooks/stop-gate.ts +0 -22
- package/hooks/stop_dispatcher.ts +0 -147
- package/hooks/test-generator-hook.ts +0 -4
- package/hooks/tool-ledger.ts +0 -27
- package/hooks/trust_review.ts +0 -175
- package/lab/pipeline.ts +0 -75
- package/lab/policies.ts +0 -68
- package/runtime/common.ts +0 -111
- package/runtime/compat.ts +0 -174
- package/runtime/dispatcher.ts +0 -25
- package/runtime/ecosystem.ts +0 -186
- package/runtime/provider_bootstrap.ts +0 -99
- package/runtime/provider_smoke.ts +0 -34
- package/runtime/release_readiness.ts +0 -186
- package/runtime/team_router.ts +0 -144
- package/scripts/check-omg-compat-contract-snapshot.ts +0 -20
- package/scripts/check-omg-standalone-clean.ts +0 -12
- package/scripts/check-runtime-clean.ts +0 -94
- package/scripts/omg.ts +0 -352
- package/scripts/settings-merge.ts +0 -93
- package/tools/commit_splitter.ts +0 -23
- package/tools/git_inspector.ts +0 -18
- package/tools/session_snapshot.ts +0 -47
- package/trac3er-oh-my-god-2.0.0.tgz +0 -0
- 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
|
+
}
|