@trac3er/oh-my-god 1.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 +36 -0
- package/.claude-plugin/plugin.json +23 -0
- package/.claude-plugin/scripts/install.sh +49 -0
- package/.claude-plugin/scripts/uninstall.sh +80 -0
- package/.claude-plugin/scripts/update.sh +84 -0
- package/.mcp.json +20 -0
- package/LICENSE +21 -0
- package/OMG-setup.sh +1093 -0
- package/README.md +335 -0
- package/THIRD_PARTY_NOTICES.md +24 -0
- package/UPSTREAM_DIFF.md +20 -0
- package/agents/__init__.py +1 -0
- package/agents/_model_roles.yaml +26 -0
- package/agents/designer.md +67 -0
- package/agents/explore.md +60 -0
- package/agents/model_roles.py +196 -0
- package/agents/omg-api-builder.md +23 -0
- package/agents/omg-architect-mode.md +43 -0
- package/agents/omg-architect.md +13 -0
- package/agents/omg-backend-engineer.md +43 -0
- package/agents/omg-critic.md +16 -0
- package/agents/omg-database-engineer.md +43 -0
- package/agents/omg-escalation-router.md +17 -0
- package/agents/omg-executor.md +12 -0
- package/agents/omg-frontend-designer.md +42 -0
- package/agents/omg-implement-mode.md +50 -0
- package/agents/omg-infra-engineer.md +43 -0
- package/agents/omg-qa-tester.md +16 -0
- package/agents/omg-research-mode.md +43 -0
- package/agents/omg-security-auditor.md +43 -0
- package/agents/omg-testing-engineer.md +43 -0
- package/agents/plan.md +80 -0
- package/agents/quick_task.md +64 -0
- package/agents/reviewer.md +83 -0
- package/agents/task.md +71 -0
- package/commands/OMG:ccg.md +22 -0
- package/commands/OMG:compat.md +57 -0
- package/commands/OMG:crazy.md +125 -0
- package/commands/OMG:domain-init.md +11 -0
- package/commands/OMG:escalate.md +52 -0
- package/commands/OMG:health-check.md +45 -0
- package/commands/OMG:init.md +134 -0
- package/commands/OMG:mode.md +44 -0
- package/commands/OMG:project-init.md +11 -0
- package/commands/OMG:ralph-start.md +43 -0
- package/commands/OMG:ralph-stop.md +23 -0
- package/commands/OMG:teams.md +39 -0
- package/commands/ai-commit.md +113 -0
- package/commands/ccg.md +9 -0
- package/commands/create-agent.md +183 -0
- package/commands/omc-teams.md +9 -0
- package/commands/session-branch.md +85 -0
- package/commands/session-fork.md +53 -0
- package/commands/session-merge.md +134 -0
- package/commands/theme.md +44 -0
- package/config/lsp_languages.yaml +324 -0
- package/config/themes/catppuccin-frappe.yaml +14 -0
- package/config/themes/catppuccin-latte.yaml +14 -0
- package/config/themes/catppuccin-macchiato.yaml +14 -0
- package/config/themes/catppuccin-mocha.yaml +14 -0
- package/config/themes/dracula.yaml +14 -0
- package/config/themes/gruvbox-dark.yaml +14 -0
- package/config/themes/nord.yaml +14 -0
- package/config/themes/one-dark.yaml +14 -0
- package/config/themes/solarized-dark.yaml +14 -0
- package/config/themes/tokyo-night.yaml +14 -0
- package/control_plane/__init__.py +2 -0
- package/control_plane/openapi.yaml +109 -0
- package/control_plane/server.py +107 -0
- package/control_plane/service.py +148 -0
- package/crates/omg-natives/Cargo.toml +17 -0
- package/crates/omg-natives/src/clipboard.rs +5 -0
- package/crates/omg-natives/src/glob.rs +15 -0
- package/crates/omg-natives/src/grep.rs +15 -0
- package/crates/omg-natives/src/highlight.rs +15 -0
- package/crates/omg-natives/src/html.rs +14 -0
- package/crates/omg-natives/src/image.rs +5 -0
- package/crates/omg-natives/src/keys.rs +5 -0
- package/crates/omg-natives/src/lib.rs +36 -0
- package/crates/omg-natives/src/prof.rs +5 -0
- package/crates/omg-natives/src/ps.rs +5 -0
- package/crates/omg-natives/src/shell.rs +5 -0
- package/crates/omg-natives/src/task.rs +5 -0
- package/crates/omg-natives/src/text.rs +14 -0
- package/hooks/_agent_registry.py +421 -0
- package/hooks/_budget.py +31 -0
- package/hooks/_common.py +476 -0
- package/hooks/_learnings.py +126 -0
- package/hooks/_memory.py +103 -0
- package/hooks/circuit-breaker.py +270 -0
- package/hooks/config-guard.py +163 -0
- package/hooks/context_pressure.py +53 -0
- package/hooks/credential_store.py +801 -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 +310 -0
- package/hooks/post-tool-failure.py +19 -0
- package/hooks/post-write.py +199 -0
- package/hooks/pre-compact.py +204 -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/secret-guard.py +47 -0
- package/hooks/session-end-capture.py +137 -0
- package/hooks/session-start.py +275 -0
- package/hooks/shadow_manager.py +297 -0
- package/hooks/state_migration.py +209 -0
- package/hooks/stop-gate.py +7 -0
- package/hooks/stop_dispatcher.py +929 -0
- package/hooks/test-validator.py +138 -0
- package/hooks/todo-state-tracker.py +114 -0
- package/hooks/tool-ledger.py +126 -0
- package/hooks/trust_review.py +524 -0
- package/install.sh +9 -0
- package/omg_natives/__init__.py +186 -0
- package/omg_natives/_bindings.py +165 -0
- package/omg_natives/clipboard.py +36 -0
- package/omg_natives/glob.py +42 -0
- package/omg_natives/grep.py +61 -0
- package/omg_natives/highlight.py +54 -0
- package/omg_natives/html.py +157 -0
- package/omg_natives/image.py +51 -0
- package/omg_natives/keys.py +46 -0
- package/omg_natives/prof.py +39 -0
- package/omg_natives/ps.py +93 -0
- package/omg_natives/shell.py +58 -0
- package/omg_natives/task.py +41 -0
- package/omg_natives/text.py +50 -0
- package/package.json +26 -0
- package/plugins/README.md +82 -0
- package/plugins/advanced/commands/OMG:code-review.md +114 -0
- package/plugins/advanced/commands/OMG:deep-plan.md +221 -0
- package/plugins/advanced/commands/OMG:handoff.md +115 -0
- package/plugins/advanced/commands/OMG:learn.md +110 -0
- package/plugins/advanced/commands/OMG:maintainer.md +31 -0
- package/plugins/advanced/commands/OMG:ralph-start.md +43 -0
- package/plugins/advanced/commands/OMG:ralph-stop.md +23 -0
- package/plugins/advanced/commands/OMG:security-review.md +119 -0
- package/plugins/advanced/commands/OMG:sequential-thinking.md +20 -0
- package/plugins/advanced/commands/OMG:ship.md +46 -0
- package/plugins/advanced/plugin.json +96 -0
- package/plugins/core/plugin.json +82 -0
- package/pytest.ini +5 -0
- package/registry/__init__.py +1 -0
- package/registry/verify_artifact.py +90 -0
- package/rules/contextual/architect-mode.md +9 -0
- package/rules/contextual/big-picture.md +20 -0
- package/rules/contextual/code-hygiene.md +26 -0
- package/rules/contextual/context-management.md +19 -0
- package/rules/contextual/context-minimization.md +32 -0
- package/rules/contextual/ddd-sdd.md +28 -0
- package/rules/contextual/dependency-safety.md +16 -0
- package/rules/contextual/doc-check.md +13 -0
- package/rules/contextual/implement-mode.md +9 -0
- package/rules/contextual/infra-safety.md +14 -0
- package/rules/contextual/outside-in.md +13 -0
- package/rules/contextual/persistent-mode.md +24 -0
- package/rules/contextual/research-mode.md +9 -0
- package/rules/contextual/security-domains.md +25 -0
- package/rules/contextual/vision-detection.md +27 -0
- package/rules/contextual/web-search.md +25 -0
- package/rules/contextual/write-verify.md +23 -0
- package/rules/core/00-truth.md +20 -0
- package/rules/core/01-surgical.md +19 -0
- package/rules/core/02-circuit-breaker.md +22 -0
- package/rules/core/03-ensemble.md +28 -0
- package/rules/core/04-testing.md +30 -0
- 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/business_workflow.py +220 -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/omc_compat.py +7 -0
- package/runtime/omc_contract_snapshot.json +916 -0
- package/runtime/omg_compat_contract_snapshot.json +916 -0
- package/runtime/subagent_dispatcher.py +362 -0
- package/runtime/team_router.py +838 -0
- package/scripts/check-omc-contract-snapshot.py +12 -0
- package/scripts/check-omg-compat-contract-snapshot.py +137 -0
- package/scripts/check-omg-standalone-clean.py +102 -0
- package/scripts/legacy_to_omg_migrate.py +29 -0
- package/scripts/migrate-omc.py +464 -0
- package/scripts/omc_to_omg_migrate.py +12 -0
- package/scripts/omg.py +493 -0
- package/scripts/settings-merge.py +224 -0
- package/scripts/verify-no-omc.sh +5 -0
- package/scripts/verify-standalone.sh +21 -0
- package/templates/idea.yml +30 -0
- package/templates/policy.yaml +15 -0
- package/templates/profile.yaml +25 -0
- package/templates/runtime.yaml +12 -0
- package/templates/working-memory.md +17 -0
- 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 +268 -0
- package/tools/commit_splitter.py +361 -0
- package/tools/config_discovery.py +151 -0
- package/tools/config_merger.py +449 -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/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
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
AI Commit Splitter for OMG
|
|
4
|
+
|
|
5
|
+
Analyzes git changes and groups them into logical atomic commits
|
|
6
|
+
with hunk-level staging support. Read-only analysis — never runs git commit.
|
|
7
|
+
|
|
8
|
+
Feature flag: OMG_AI_COMMIT_ENABLED (default: False)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
# Lazy imports for git_inspector and feature flags
|
|
17
|
+
_git_inspector = None
|
|
18
|
+
_get_feature_flag = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _ensure_imports():
|
|
22
|
+
"""Lazy import git_inspector and feature flag helper."""
|
|
23
|
+
global _git_inspector, _get_feature_flag
|
|
24
|
+
if _git_inspector is None:
|
|
25
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
26
|
+
from tools import git_inspector as _gi
|
|
27
|
+
from hooks._common import get_feature_flag as _gff
|
|
28
|
+
_git_inspector = _gi
|
|
29
|
+
_get_feature_flag = _gff
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _is_enabled() -> bool:
|
|
33
|
+
"""Check if AI commit splitter feature is enabled."""
|
|
34
|
+
_ensure_imports()
|
|
35
|
+
return _get_feature_flag("ai_commit", default=False)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# --- File Classification ---
|
|
39
|
+
|
|
40
|
+
# Extension → category mapping
|
|
41
|
+
_EXT_CATEGORY = {
|
|
42
|
+
# Python
|
|
43
|
+
".py": "python",
|
|
44
|
+
".pyi": "python",
|
|
45
|
+
".pyx": "python",
|
|
46
|
+
# JavaScript/TypeScript
|
|
47
|
+
".js": "javascript",
|
|
48
|
+
".jsx": "javascript",
|
|
49
|
+
".ts": "javascript",
|
|
50
|
+
".tsx": "javascript",
|
|
51
|
+
".mjs": "javascript",
|
|
52
|
+
".cjs": "javascript",
|
|
53
|
+
# Config
|
|
54
|
+
".json": "config",
|
|
55
|
+
".yaml": "config",
|
|
56
|
+
".yml": "config",
|
|
57
|
+
".toml": "config",
|
|
58
|
+
".ini": "config",
|
|
59
|
+
".cfg": "config",
|
|
60
|
+
".env": "config",
|
|
61
|
+
".conf": "config",
|
|
62
|
+
".properties": "config",
|
|
63
|
+
# Docs
|
|
64
|
+
".md": "docs",
|
|
65
|
+
".rst": "docs",
|
|
66
|
+
".txt": "docs",
|
|
67
|
+
".adoc": "docs",
|
|
68
|
+
# Shell
|
|
69
|
+
".sh": "shell",
|
|
70
|
+
".bash": "shell",
|
|
71
|
+
".zsh": "shell",
|
|
72
|
+
# CSS/Styles
|
|
73
|
+
".css": "styles",
|
|
74
|
+
".scss": "styles",
|
|
75
|
+
".less": "styles",
|
|
76
|
+
".sass": "styles",
|
|
77
|
+
# HTML/Templates
|
|
78
|
+
".html": "markup",
|
|
79
|
+
".htm": "markup",
|
|
80
|
+
".xml": "markup",
|
|
81
|
+
".svg": "markup",
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Test path indicators
|
|
85
|
+
_TEST_INDICATORS = (
|
|
86
|
+
"test_",
|
|
87
|
+
"_test.",
|
|
88
|
+
"tests/",
|
|
89
|
+
"test/",
|
|
90
|
+
"__tests__/",
|
|
91
|
+
".test.",
|
|
92
|
+
".spec.",
|
|
93
|
+
"spec/",
|
|
94
|
+
"conftest.py",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Category → default suggested commit type
|
|
98
|
+
_CATEGORY_DEFAULT_TYPE = {
|
|
99
|
+
"python": "feat",
|
|
100
|
+
"javascript": "feat",
|
|
101
|
+
"shell": "chore",
|
|
102
|
+
"config": "chore",
|
|
103
|
+
"docs": "docs",
|
|
104
|
+
"styles": "style",
|
|
105
|
+
"markup": "feat",
|
|
106
|
+
"tests": "test",
|
|
107
|
+
"other": "chore",
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _classify_file(file_path: str) -> str:
|
|
112
|
+
"""Classify a file path into a category.
|
|
113
|
+
|
|
114
|
+
Test files are always classified as 'tests' regardless of extension.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
file_path: Relative file path from git diff.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Category string: 'python', 'javascript', 'config', 'docs',
|
|
121
|
+
'tests', 'shell', 'styles', 'markup', or 'other'.
|
|
122
|
+
"""
|
|
123
|
+
if file_path is None:
|
|
124
|
+
return "other"
|
|
125
|
+
|
|
126
|
+
lower_path = file_path.lower()
|
|
127
|
+
|
|
128
|
+
# Check for test files first — they always get their own group
|
|
129
|
+
for indicator in _TEST_INDICATORS:
|
|
130
|
+
if indicator in lower_path:
|
|
131
|
+
return "tests"
|
|
132
|
+
|
|
133
|
+
# Classify by extension
|
|
134
|
+
_, ext = os.path.splitext(lower_path)
|
|
135
|
+
return _EXT_CATEGORY.get(ext, "other")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _derive_scope(files: List[str]) -> str:
|
|
139
|
+
"""Derive a scope name from a list of files.
|
|
140
|
+
|
|
141
|
+
Uses the most common parent directory or module name.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
files: List of file paths.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Scope string suitable for conventional commit format.
|
|
148
|
+
"""
|
|
149
|
+
if not files:
|
|
150
|
+
return "general"
|
|
151
|
+
|
|
152
|
+
# Collect directory components
|
|
153
|
+
dirs: List[str] = []
|
|
154
|
+
for f in files:
|
|
155
|
+
parts = f.replace("\\", "/").split("/")
|
|
156
|
+
if len(parts) > 1:
|
|
157
|
+
dirs.append(parts[0])
|
|
158
|
+
else:
|
|
159
|
+
# Single file at root — use filename without extension
|
|
160
|
+
name, _ = os.path.splitext(parts[0])
|
|
161
|
+
dirs.append(name)
|
|
162
|
+
|
|
163
|
+
if not dirs:
|
|
164
|
+
return "general"
|
|
165
|
+
|
|
166
|
+
# Most common directory
|
|
167
|
+
from collections import Counter
|
|
168
|
+
counts = Counter(dirs)
|
|
169
|
+
scope = counts.most_common(1)[0][0]
|
|
170
|
+
return scope
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _derive_description(category: str, files: List[str]) -> str:
|
|
174
|
+
"""Generate a short description for a commit group.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
category: File category (e.g., 'python', 'tests').
|
|
178
|
+
files: List of affected files.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Human-readable description string.
|
|
182
|
+
"""
|
|
183
|
+
n = len(files)
|
|
184
|
+
if category == "tests":
|
|
185
|
+
if n == 1:
|
|
186
|
+
return f"update test {os.path.basename(files[0])}"
|
|
187
|
+
return f"update {n} test files"
|
|
188
|
+
if category == "docs":
|
|
189
|
+
if n == 1:
|
|
190
|
+
return f"update {os.path.basename(files[0])}"
|
|
191
|
+
return f"update {n} documentation files"
|
|
192
|
+
if category == "config":
|
|
193
|
+
if n == 1:
|
|
194
|
+
return f"update {os.path.basename(files[0])}"
|
|
195
|
+
return f"update {n} config files"
|
|
196
|
+
# Source code
|
|
197
|
+
if n == 1:
|
|
198
|
+
return f"update {os.path.basename(files[0])}"
|
|
199
|
+
return f"update {n} {category} files"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# --- Public API ---
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def analyze_changes(cwd: str = ".") -> List[Dict[str, Any]]:
|
|
206
|
+
"""Analyze git changes and group hunks by logical concern.
|
|
207
|
+
|
|
208
|
+
Groups changes by file type/category, always separating test files
|
|
209
|
+
from source code. Returns an empty list when the feature flag
|
|
210
|
+
``OMG_AI_COMMIT_ENABLED`` is ``False``.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
cwd: Working directory (default: current directory).
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
List of dicts, each with keys:
|
|
217
|
+
- group_name (str): Human-readable group name.
|
|
218
|
+
- files (list[str]): Affected file paths.
|
|
219
|
+
- hunks (list[dict]): Raw hunk dicts from git_hunk().
|
|
220
|
+
- suggested_type (str): Conventional commit type.
|
|
221
|
+
"""
|
|
222
|
+
if not _is_enabled():
|
|
223
|
+
return []
|
|
224
|
+
|
|
225
|
+
_ensure_imports()
|
|
226
|
+
hunks = _git_inspector.git_hunk(cwd)
|
|
227
|
+
|
|
228
|
+
if not hunks:
|
|
229
|
+
return []
|
|
230
|
+
|
|
231
|
+
# Bucket hunks by category
|
|
232
|
+
buckets: Dict[str, Dict[str, Any]] = {}
|
|
233
|
+
for hunk in hunks:
|
|
234
|
+
file_path = hunk.get("file", "")
|
|
235
|
+
category = _classify_file(file_path)
|
|
236
|
+
|
|
237
|
+
if category not in buckets:
|
|
238
|
+
buckets[category] = {
|
|
239
|
+
"files_set": set(),
|
|
240
|
+
"hunks": [],
|
|
241
|
+
}
|
|
242
|
+
buckets[category]["files_set"].add(file_path)
|
|
243
|
+
buckets[category]["hunks"].append(hunk)
|
|
244
|
+
|
|
245
|
+
# Build result groups
|
|
246
|
+
groups: List[Dict[str, Any]] = []
|
|
247
|
+
for category, data in sorted(buckets.items()):
|
|
248
|
+
files = sorted(data["files_set"])
|
|
249
|
+
groups.append({
|
|
250
|
+
"group_name": category,
|
|
251
|
+
"files": files,
|
|
252
|
+
"hunks": data["hunks"],
|
|
253
|
+
"suggested_type": _CATEGORY_DEFAULT_TYPE.get(category, "chore"),
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
return groups
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def generate_commit_plan(cwd: str = ".") -> Dict[str, Any]:
|
|
260
|
+
"""Generate a full commit plan with proposed messages.
|
|
261
|
+
|
|
262
|
+
Calls ``analyze_changes()`` and builds conventional commit messages
|
|
263
|
+
for each group. Returns an empty plan when the feature flag is off.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
cwd: Working directory (default: current directory).
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Dict with keys:
|
|
270
|
+
- groups (list[dict]): Raw groups from analyze_changes().
|
|
271
|
+
- proposed_commits (list[dict]): Each with ``message``,
|
|
272
|
+
``files``, and ``hunks``.
|
|
273
|
+
- total_commits (int): Number of proposed commits.
|
|
274
|
+
"""
|
|
275
|
+
groups = analyze_changes(cwd)
|
|
276
|
+
|
|
277
|
+
if not groups:
|
|
278
|
+
return {
|
|
279
|
+
"groups": [],
|
|
280
|
+
"proposed_commits": [],
|
|
281
|
+
"total_commits": 0,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
proposed: List[Dict[str, Any]] = []
|
|
285
|
+
for group in groups:
|
|
286
|
+
commit_type = group["suggested_type"]
|
|
287
|
+
scope = _derive_scope(group["files"])
|
|
288
|
+
description = _derive_description(group["group_name"], group["files"])
|
|
289
|
+
message = f"{commit_type}({scope}): {description}"
|
|
290
|
+
|
|
291
|
+
proposed.append({
|
|
292
|
+
"message": message,
|
|
293
|
+
"files": group["files"],
|
|
294
|
+
"hunks": group["hunks"],
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
"groups": groups,
|
|
299
|
+
"proposed_commits": proposed,
|
|
300
|
+
"total_commits": len(proposed),
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def preview_commit_plan(cwd: str = ".") -> str:
|
|
305
|
+
"""Human-readable preview of the commit plan.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
cwd: Working directory (default: current directory).
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Formatted string showing each proposed commit and affected files.
|
|
312
|
+
Returns a notice string if the feature flag is off or no changes found.
|
|
313
|
+
"""
|
|
314
|
+
plan = generate_commit_plan(cwd)
|
|
315
|
+
|
|
316
|
+
if plan["total_commits"] == 0:
|
|
317
|
+
if not _is_enabled():
|
|
318
|
+
return "[OMG] AI Commit Splitter is disabled. Set OMG_AI_COMMIT_ENABLED=1 to enable."
|
|
319
|
+
return "[OMG] No uncommitted changes found."
|
|
320
|
+
|
|
321
|
+
lines: List[str] = []
|
|
322
|
+
lines.append("=" * 60)
|
|
323
|
+
lines.append(" OMG AI Commit Splitter — Proposed Commit Plan")
|
|
324
|
+
lines.append("=" * 60)
|
|
325
|
+
lines.append("")
|
|
326
|
+
lines.append(f" Total proposed commits: {plan['total_commits']}")
|
|
327
|
+
lines.append("")
|
|
328
|
+
|
|
329
|
+
for idx, commit in enumerate(plan["proposed_commits"], 1):
|
|
330
|
+
lines.append(f" Commit {idx}: {commit['message']}")
|
|
331
|
+
lines.append(f" {'─' * 50}")
|
|
332
|
+
for f in commit["files"]:
|
|
333
|
+
lines.append(f" • {f}")
|
|
334
|
+
hunk_count = len(commit["hunks"])
|
|
335
|
+
lines.append(f" ({hunk_count} hunk{'s' if hunk_count != 1 else ''})")
|
|
336
|
+
lines.append("")
|
|
337
|
+
|
|
338
|
+
lines.append("=" * 60)
|
|
339
|
+
lines.append(" NOTE: This is a preview only. No commits were made.")
|
|
340
|
+
lines.append("=" * 60)
|
|
341
|
+
|
|
342
|
+
return "\n".join(lines)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
# --- CLI ---
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def main():
|
|
349
|
+
"""CLI entry point."""
|
|
350
|
+
if len(sys.argv) < 2 or sys.argv[1] != "--dry-run":
|
|
351
|
+
print("Usage:", file=sys.stderr)
|
|
352
|
+
print(" python3 tools/commit_splitter.py --dry-run", file=sys.stderr)
|
|
353
|
+
sys.exit(1)
|
|
354
|
+
|
|
355
|
+
cwd = os.getcwd()
|
|
356
|
+
output = preview_commit_plan(cwd)
|
|
357
|
+
print(output)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
if __name__ == "__main__":
|
|
361
|
+
main()
|
|
@@ -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()
|