@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,151 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Config Discovery Framework for AI Coding Tools
4
+
5
+ Scans a project directory for configuration files from 8 AI coding tools
6
+ and produces a JSON discovery report. Read-only operation.
7
+
8
+ Feature flag: OMG_CONFIG_DISCOVERY_ENABLED (default: off)
9
+ """
10
+
11
+ import json
12
+ import os
13
+ import sys
14
+ from datetime import datetime
15
+ from pathlib import Path
16
+ from typing import Any, Dict, List
17
+
18
+ # Feature flag
19
+ OMG_CONFIG_DISCOVERY_ENABLED = os.getenv("OMG_CONFIG_DISCOVERY_ENABLED", "false").lower() == "true"
20
+
21
+ # Tool detection patterns
22
+ TOOL_PATTERNS = {
23
+ "claude_code": [".claude/", ".claude/CLAUDE.md", "CLAUDE.md"],
24
+ "cursor": [".cursorrules", ".cursor/rules/", ".cursor/"],
25
+ "windsurf": [".windsurf/", ".windsurfrules"],
26
+ "gemini": ["system.md", ".gemini/"],
27
+ "codex": ["AGENTS.md"],
28
+ "cline": [".clinerules"],
29
+ "github_copilot": [".github/copilot-instructions.md"],
30
+ "vscode": [".vscode/settings.json", ".vscode/"],
31
+ }
32
+
33
+
34
+ def get_file_size(path: Path) -> int:
35
+ """Get file size in bytes. Returns 0 if not a file."""
36
+ try:
37
+ if path.is_file():
38
+ return path.stat().st_size
39
+ return 0
40
+ except (OSError, ValueError):
41
+ return 0
42
+
43
+
44
+ def is_readable(path: Path) -> bool:
45
+ """Check if path is readable."""
46
+ try:
47
+ return os.access(str(path), os.R_OK)
48
+ except (OSError, ValueError):
49
+ return False
50
+
51
+
52
+ def get_format(path: Path) -> str:
53
+ """Determine file format from extension or path."""
54
+ if path.is_dir():
55
+ return "directory"
56
+
57
+ suffix = path.suffix.lower()
58
+ if suffix == ".md":
59
+ return "markdown"
60
+ elif suffix == ".json":
61
+ return "json"
62
+ elif suffix == ".yaml" or suffix == ".yml":
63
+ return "yaml"
64
+ elif suffix == ".txt":
65
+ return "text"
66
+ else:
67
+ return "unknown"
68
+
69
+
70
+ def discover_configs(project_dir: str) -> Dict[str, Any]:
71
+ """
72
+ Scan project_dir for AI tool configs.
73
+
74
+ Returns:
75
+ {
76
+ "discovered": [
77
+ {
78
+ "tool": "claude_code",
79
+ "paths": [".claude/CLAUDE.md"],
80
+ "format": "markdown",
81
+ "size_bytes": 1234,
82
+ "readable": true
83
+ },
84
+ ...
85
+ ],
86
+ "scan_dir": "/path/to/project",
87
+ "timestamp": "2025-03-02T10:30:45.123456"
88
+ }
89
+ """
90
+ project_path = Path(project_dir).resolve()
91
+
92
+ if not project_path.exists():
93
+ return {
94
+ "discovered": [],
95
+ "scan_dir": str(project_path),
96
+ "timestamp": datetime.now().isoformat(),
97
+ "error": f"Project directory does not exist: {project_dir}"
98
+ }
99
+
100
+ discovered = []
101
+
102
+ for tool_name, patterns in TOOL_PATTERNS.items():
103
+ tool_paths = []
104
+
105
+ for pattern in patterns:
106
+ # Handle directory patterns (ending with /)
107
+ if pattern.endswith("/"):
108
+ dir_path = project_path / pattern.rstrip("/")
109
+ if dir_path.exists() and dir_path.is_dir():
110
+ tool_paths.append(pattern.rstrip("/"))
111
+ else:
112
+ # Handle file patterns
113
+ file_path = project_path / pattern
114
+ if file_path.exists():
115
+ tool_paths.append(pattern)
116
+
117
+ if tool_paths:
118
+ # Get info from first discovered path
119
+ first_path = project_path / tool_paths[0]
120
+ size_bytes = get_file_size(first_path)
121
+ readable = is_readable(first_path)
122
+ format_type = get_format(first_path)
123
+
124
+ discovered.append({
125
+ "tool": tool_name,
126
+ "paths": tool_paths,
127
+ "format": format_type,
128
+ "size_bytes": size_bytes,
129
+ "readable": readable
130
+ })
131
+
132
+ return {
133
+ "discovered": discovered,
134
+ "scan_dir": str(project_path),
135
+ "timestamp": datetime.now().isoformat()
136
+ }
137
+
138
+
139
+ def main():
140
+ """CLI entry point."""
141
+ if len(sys.argv) < 3 or sys.argv[1] != "--scan":
142
+ print("Usage: python3 config_discovery.py --scan <directory>", file=sys.stderr)
143
+ sys.exit(1)
144
+
145
+ project_dir = sys.argv[2]
146
+ result = discover_configs(project_dir)
147
+ print(json.dumps(result, indent=2))
148
+
149
+
150
+ if __name__ == "__main__":
151
+ main()
@@ -0,0 +1,449 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Config Merging Framework for OMG
4
+
5
+ Merges discovered AI tool configurations into a unified OMG config with
6
+ priority-based conflict resolution.
7
+
8
+ Priority order (highest to lowest):
9
+ 1. OMG config (.omg/state/omg_config.json)
10
+ 2. Project-level configs (discovered in project directory)
11
+ 3. User-level configs (discovered in home directory)
12
+ 4. Tool defaults
13
+
14
+ Feature flag: OMG_CONFIG_DISCOVERY_ENABLED (default: off)
15
+ """
16
+
17
+ import json
18
+ import os
19
+ import sys
20
+ from datetime import datetime, timezone
21
+ from typing import Any, Dict, List, Tuple
22
+
23
+
24
+ # Priority levels — lower number = higher priority
25
+ PRIORITY_OMG = 0
26
+ PRIORITY_PROJECT = 10
27
+ PRIORITY_USER = 20
28
+ PRIORITY_DEFAULT = 30
29
+
30
+ # Source type labels
31
+ SOURCE_OMG = "omg_config"
32
+ SOURCE_PROJECT = "project"
33
+ SOURCE_USER = "user"
34
+ SOURCE_DEFAULT = "default"
35
+
36
+
37
+ def _get_feature_flag_enabled() -> bool:
38
+ """Check if config discovery feature is enabled.
39
+
40
+ Resolution: env var → _common.get_feature_flag() → False.
41
+ """
42
+ env_val = os.environ.get("OMG_CONFIG_DISCOVERY_ENABLED", "").lower()
43
+ if env_val in ("0", "false", "no"):
44
+ return False
45
+ if env_val in ("1", "true", "yes"):
46
+ return True
47
+
48
+ # Lazy import from hooks
49
+ hooks_dir = os.path.normpath(
50
+ os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "hooks")
51
+ )
52
+ if hooks_dir not in sys.path:
53
+ sys.path.insert(0, hooks_dir)
54
+ try:
55
+ from _common import get_feature_flag # type: ignore[import-untyped]
56
+
57
+ return get_feature_flag("CONFIG_DISCOVERY", default=False)
58
+ except ImportError:
59
+ return False
60
+
61
+
62
+ def _get_atomic_json_write():
63
+ """Lazy-import atomic_json_write from hooks/_common.py."""
64
+ hooks_dir = os.path.normpath(
65
+ os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "hooks")
66
+ )
67
+ if hooks_dir not in sys.path:
68
+ sys.path.insert(0, hooks_dir)
69
+ try:
70
+ from _common import atomic_json_write # type: ignore[import-untyped]
71
+
72
+ return atomic_json_write
73
+ except ImportError:
74
+ return None
75
+
76
+
77
+ def _extract_config_values(config_path: str, fmt: str) -> Dict[str, Any]:
78
+ """Extract key-value pairs from a config file.
79
+
80
+ Handles JSON, YAML (if available), TOML (if available), and
81
+ markdown (extracts frontmatter).
82
+
83
+ Returns empty dict on any parse error — never crashes.
84
+ """
85
+ if not config_path or not os.path.isfile(config_path):
86
+ return {}
87
+
88
+ try:
89
+ with open(config_path, "r", encoding="utf-8", errors="ignore") as f:
90
+ content = f.read(256 * 1024) # 256KB limit
91
+ except (OSError, IOError):
92
+ return {}
93
+
94
+ if not content.strip():
95
+ return {}
96
+
97
+ # JSON
98
+ if fmt == "json":
99
+ return _parse_json(content)
100
+
101
+ # YAML
102
+ if fmt in ("yaml", "yml"):
103
+ return _parse_yaml(content)
104
+
105
+ # TOML
106
+ if fmt == "toml":
107
+ return _parse_toml(content)
108
+
109
+ # Markdown — extract frontmatter
110
+ if fmt == "markdown":
111
+ return _parse_markdown_frontmatter(content)
112
+
113
+ # Unknown format — try JSON first, then YAML
114
+ result = _parse_json(content)
115
+ if result:
116
+ return result
117
+ return _parse_yaml(content)
118
+
119
+
120
+ def _parse_json(content: str) -> Dict[str, Any]:
121
+ """Parse JSON content into a dict."""
122
+ try:
123
+ data = json.loads(content)
124
+ return data if isinstance(data, dict) else {}
125
+ except (json.JSONDecodeError, ValueError):
126
+ return {}
127
+
128
+
129
+ def _parse_yaml(content: str) -> Dict[str, Any]:
130
+ """Parse YAML content. Returns empty dict if PyYAML not available."""
131
+ try:
132
+ import yaml # type: ignore[import-untyped]
133
+
134
+ data = yaml.safe_load(content)
135
+ return data if isinstance(data, dict) else {}
136
+ except ImportError:
137
+ return {}
138
+ except Exception:
139
+ return {}
140
+
141
+
142
+ def _parse_toml(content: str) -> Dict[str, Any]:
143
+ """Parse TOML content. Tries tomllib (3.11+), then tomli, then fails gracefully."""
144
+ try:
145
+ import tomllib # type: ignore[import-not-found] # Python 3.11+
146
+
147
+ data = tomllib.loads(content)
148
+ return data if isinstance(data, dict) else {}
149
+ except ImportError:
150
+ pass
151
+ except Exception:
152
+ return {}
153
+
154
+ try:
155
+ import tomli # type: ignore[import-untyped]
156
+
157
+ data = tomli.loads(content)
158
+ return data if isinstance(data, dict) else {}
159
+ except ImportError:
160
+ return {}
161
+ except Exception:
162
+ return {}
163
+
164
+
165
+ def _parse_markdown_frontmatter(content: str) -> Dict[str, Any]:
166
+ """Extract YAML frontmatter from markdown (between --- delimiters)."""
167
+ content = content.strip()
168
+ if not content.startswith("---"):
169
+ return {}
170
+
171
+ # Find the closing ---
172
+ end_idx = content.find("---", 3)
173
+ if end_idx == -1:
174
+ return {}
175
+
176
+ frontmatter = content[3:end_idx].strip()
177
+ if not frontmatter:
178
+ return {}
179
+
180
+ return _parse_yaml(frontmatter)
181
+
182
+
183
+ def _classify_source(config: Dict[str, Any]) -> str:
184
+ """Classify a discovered config as project-level or user-level.
185
+
186
+ Heuristic: configs from home directory paths are user-level,
187
+ everything else is project-level.
188
+ """
189
+ paths = config.get("paths", [])
190
+ if not paths:
191
+ return SOURCE_PROJECT
192
+
193
+ first_path = str(paths[0])
194
+ home = os.path.expanduser("~")
195
+
196
+ # If the path is absolute and under home dir (not in project), it's user-level
197
+ if os.path.isabs(first_path) and first_path.startswith(home):
198
+ return SOURCE_USER
199
+
200
+ return SOURCE_PROJECT
201
+
202
+
203
+ def _get_priority(source_type: str) -> int:
204
+ """Get numeric priority for a source type. Lower = higher priority."""
205
+ return {
206
+ SOURCE_OMG: PRIORITY_OMG,
207
+ SOURCE_PROJECT: PRIORITY_PROJECT,
208
+ SOURCE_USER: PRIORITY_USER,
209
+ SOURCE_DEFAULT: PRIORITY_DEFAULT,
210
+ }.get(source_type, PRIORITY_DEFAULT)
211
+
212
+
213
+ def _resolve_conflict(
214
+ key: str,
215
+ existing_val: Any,
216
+ new_val: Any,
217
+ existing_source: str,
218
+ new_source: str,
219
+ ) -> Tuple[Any, Dict[str, Any]]:
220
+ """Resolve a config key conflict between two sources.
221
+
222
+ Priority rules: higher priority source (lower number) wins.
223
+ If same priority, last-write-wins (new_val wins).
224
+
225
+ Returns:
226
+ (winning_value, conflict_record) where conflict_record is a dict
227
+ documenting the conflict for logging.
228
+ """
229
+ existing_priority = _get_priority(existing_source)
230
+ new_priority = _get_priority(new_source)
231
+
232
+ conflict_record = {
233
+ "key": key,
234
+ "existing_value": existing_val,
235
+ "existing_source": existing_source,
236
+ "new_value": new_val,
237
+ "new_source": new_source,
238
+ "timestamp": datetime.now(timezone.utc).isoformat(),
239
+ }
240
+
241
+ if new_priority < existing_priority:
242
+ # New source has higher priority (lower number) — it wins
243
+ conflict_record["winner"] = new_source
244
+ conflict_record["resolution"] = "higher_priority"
245
+ return new_val, conflict_record
246
+ elif new_priority > existing_priority:
247
+ # Existing source has higher priority — it wins
248
+ conflict_record["winner"] = existing_source
249
+ conflict_record["resolution"] = "higher_priority"
250
+ return existing_val, conflict_record
251
+ else:
252
+ # Same priority — last-write-wins
253
+ conflict_record["winner"] = new_source
254
+ conflict_record["resolution"] = "last_write_wins"
255
+ return new_val, conflict_record
256
+
257
+
258
+ def _load_omg_config(omg_config_path: str) -> Dict[str, Any]:
259
+ """Load OMG config from disk. Returns empty dict on any error."""
260
+ if not omg_config_path or not os.path.isfile(omg_config_path):
261
+ return {}
262
+ try:
263
+ with open(omg_config_path, "r", encoding="utf-8") as f:
264
+ data = json.load(f)
265
+ return data if isinstance(data, dict) else {}
266
+ except (json.JSONDecodeError, OSError, IOError):
267
+ return {}
268
+
269
+
270
+ def merge_configs(
271
+ discovered_configs: List[Dict[str, Any]],
272
+ omg_config_path: str = ".omg/state/omg_config.json",
273
+ ) -> Dict[str, Any]:
274
+ """Merge discovered AI tool configs into a unified OMG config.
275
+
276
+ Args:
277
+ discovered_configs: List of config dicts from discover_configs()["discovered"].
278
+ Each dict has: tool, paths, format, size_bytes, readable.
279
+ omg_config_path: Path to existing OMG config (highest priority).
280
+
281
+ Returns:
282
+ {
283
+ "merged": dict — the merged configuration,
284
+ "conflicts": list — conflict records,
285
+ "sources": list — source descriptions,
286
+ "timestamp": str — ISO 8601 timestamp,
287
+ }
288
+ or {"skipped": True} if feature flag is disabled.
289
+ """
290
+ if not _get_feature_flag_enabled():
291
+ return {"skipped": True}
292
+
293
+ merged: Dict[str, Any] = {}
294
+ conflicts: List[Dict[str, Any]] = []
295
+ sources: List[Dict[str, Any]] = []
296
+ source_map: Dict[str, str] = {} # key → source label for conflict tracking
297
+
298
+ # Phase 1: Load OMG config (highest priority)
299
+ omg_values = _load_omg_config(omg_config_path)
300
+ if omg_values:
301
+ sources.append({
302
+ "type": SOURCE_OMG,
303
+ "path": omg_config_path,
304
+ "keys_count": len(omg_values),
305
+ })
306
+ for key, val in omg_values.items():
307
+ merged[key] = val
308
+ source_map[key] = SOURCE_OMG
309
+
310
+ # Phase 2: Process discovered configs by priority
311
+ # Sort: project-level first, then user-level
312
+ sorted_configs = sorted(
313
+ discovered_configs or [],
314
+ key=lambda c: _get_priority(_classify_source(c)),
315
+ )
316
+
317
+ for config in sorted_configs:
318
+ tool = config.get("tool", "unknown")
319
+ paths = config.get("paths", [])
320
+ fmt = config.get("format", "unknown")
321
+ readable = config.get("readable", False)
322
+
323
+ if not paths or not readable:
324
+ continue
325
+
326
+ first_path = paths[0]
327
+ source_type = _classify_source(config)
328
+ source_label = f"{source_type}:{tool}:{first_path}"
329
+
330
+ # Extract values from the config file
331
+ values = _extract_config_values(first_path, fmt)
332
+ if not values:
333
+ continue
334
+
335
+ sources.append({
336
+ "type": source_type,
337
+ "tool": tool,
338
+ "path": first_path,
339
+ "format": fmt,
340
+ "keys_count": len(values),
341
+ })
342
+
343
+ for key, val in values.items():
344
+ if key in merged:
345
+ # Conflict — resolve with priority rules
346
+ winning_val, conflict_record = _resolve_conflict(
347
+ key,
348
+ merged[key],
349
+ val,
350
+ source_map.get(key, SOURCE_DEFAULT),
351
+ source_label,
352
+ )
353
+ conflicts.append(conflict_record)
354
+ merged[key] = winning_val
355
+ if conflict_record["winner"] == source_label:
356
+ source_map[key] = source_label
357
+ else:
358
+ merged[key] = val
359
+ source_map[key] = source_label
360
+
361
+ result = {
362
+ "merged": merged,
363
+ "conflicts": conflicts,
364
+ "sources": sources,
365
+ "timestamp": datetime.now(timezone.utc).isoformat(),
366
+ }
367
+
368
+ # Persist merged config
369
+ _persist_merged_config(result)
370
+
371
+ # Persist conflict log if any conflicts
372
+ if conflicts:
373
+ _persist_conflicts(conflicts)
374
+
375
+ return result
376
+
377
+
378
+ def _persist_merged_config(result: Dict[str, Any]) -> None:
379
+ """Persist merged config to .omg/state/merged_config.json."""
380
+ writer = _get_atomic_json_write()
381
+ if writer is None:
382
+ return
383
+ try:
384
+ merged_path = os.path.join(".omg", "state", "merged_config.json")
385
+ writer(merged_path, result)
386
+ except Exception:
387
+ pass # Never crash on persistence
388
+
389
+
390
+ def _persist_conflicts(conflicts: List[Dict[str, Any]]) -> None:
391
+ """Persist conflict log to .omg/state/config_conflicts.json."""
392
+ writer = _get_atomic_json_write()
393
+ if writer is None:
394
+ return
395
+ try:
396
+ conflicts_path = os.path.join(".omg", "state", "config_conflicts.json")
397
+ writer(conflicts_path, conflicts)
398
+ except Exception:
399
+ pass # Never crash on persistence
400
+
401
+
402
+ def get_merged_config() -> Dict[str, Any]:
403
+ """Load and return the persisted merged config.
404
+
405
+ Returns the full merged config dict from .omg/state/merged_config.json,
406
+ or an empty dict if the file doesn't exist or is unreadable.
407
+ """
408
+ merged_path = os.path.join(".omg", "state", "merged_config.json")
409
+ if not os.path.isfile(merged_path):
410
+ return {}
411
+ try:
412
+ with open(merged_path, "r", encoding="utf-8") as f:
413
+ data = json.load(f)
414
+ return data if isinstance(data, dict) else {}
415
+ except (json.JSONDecodeError, OSError, IOError):
416
+ return {}
417
+
418
+
419
+ def main():
420
+ """CLI entry point."""
421
+ if len(sys.argv) < 3 or sys.argv[1] != "--merge":
422
+ print(
423
+ "Usage: python3 config_merger.py --merge <directory>",
424
+ file=sys.stderr,
425
+ )
426
+ sys.exit(1)
427
+
428
+ project_dir = sys.argv[2]
429
+
430
+ # Lazy import discover_configs
431
+ tools_dir = os.path.dirname(os.path.abspath(__file__))
432
+ if tools_dir not in sys.path:
433
+ sys.path.insert(0, tools_dir)
434
+ try:
435
+ from config_discovery import discover_configs # type: ignore[import-untyped]
436
+ except ImportError:
437
+ print("Error: config_discovery module not available", file=sys.stderr)
438
+ sys.exit(1)
439
+
440
+ discovery = discover_configs(project_dir)
441
+ discovered = discovery.get("discovered", [])
442
+
443
+ omg_config_path = os.path.join(project_dir, ".omg", "state", "omg_config.json")
444
+ result = merge_configs(discovered, omg_config_path)
445
+ print(json.dumps(result, indent=2))
446
+
447
+
448
+ if __name__ == "__main__":
449
+ main()