@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,321 @@
|
|
|
1
|
+
"""Minimal YAML compatibility layer for OMG.
|
|
2
|
+
|
|
3
|
+
This keeps standalone/no-vendor flows working when PyYAML is unavailable.
|
|
4
|
+
It supports the subset used in OMG configs: mappings, lists, quoted strings,
|
|
5
|
+
booleans, numbers, nulls, and simple flow-style collections.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import io
|
|
10
|
+
import json
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _read_text(stream_or_text: Any) -> str:
|
|
15
|
+
if hasattr(stream_or_text, "read"):
|
|
16
|
+
return str(stream_or_text.read())
|
|
17
|
+
return str(stream_or_text)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _strip_comments(line: str) -> str:
|
|
21
|
+
in_single = False
|
|
22
|
+
in_double = False
|
|
23
|
+
escaped = False
|
|
24
|
+
out: list[str] = []
|
|
25
|
+
for char in line:
|
|
26
|
+
if escaped:
|
|
27
|
+
out.append(char)
|
|
28
|
+
escaped = False
|
|
29
|
+
continue
|
|
30
|
+
if char == "\\" and in_double:
|
|
31
|
+
out.append(char)
|
|
32
|
+
escaped = True
|
|
33
|
+
continue
|
|
34
|
+
if char == "'" and not in_double:
|
|
35
|
+
in_single = not in_single
|
|
36
|
+
out.append(char)
|
|
37
|
+
continue
|
|
38
|
+
if char == '"' and not in_single:
|
|
39
|
+
in_double = not in_double
|
|
40
|
+
out.append(char)
|
|
41
|
+
continue
|
|
42
|
+
if char == "#" and not in_single and not in_double:
|
|
43
|
+
break
|
|
44
|
+
out.append(char)
|
|
45
|
+
return "".join(out).rstrip()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _prepare_lines(text: str) -> list[tuple[int, str]]:
|
|
49
|
+
prepared: list[tuple[int, str]] = []
|
|
50
|
+
for raw_line in text.splitlines():
|
|
51
|
+
stripped = _strip_comments(raw_line)
|
|
52
|
+
if not stripped.strip():
|
|
53
|
+
continue
|
|
54
|
+
indent = len(stripped) - len(stripped.lstrip(" "))
|
|
55
|
+
prepared.append((indent, stripped[indent:]))
|
|
56
|
+
return prepared
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _split_key_value(text: str) -> tuple[str, str] | None:
|
|
60
|
+
in_single = False
|
|
61
|
+
in_double = False
|
|
62
|
+
escaped = False
|
|
63
|
+
for index, char in enumerate(text):
|
|
64
|
+
if escaped:
|
|
65
|
+
escaped = False
|
|
66
|
+
continue
|
|
67
|
+
if char == "\\" and in_double:
|
|
68
|
+
escaped = True
|
|
69
|
+
continue
|
|
70
|
+
if char == "'" and not in_double:
|
|
71
|
+
in_single = not in_single
|
|
72
|
+
continue
|
|
73
|
+
if char == '"' and not in_single:
|
|
74
|
+
in_double = not in_double
|
|
75
|
+
continue
|
|
76
|
+
if char == ":" and not in_single and not in_double:
|
|
77
|
+
if index + 1 < len(text) and text[index + 1] not in {" ", "\t"}:
|
|
78
|
+
continue
|
|
79
|
+
return text[:index].strip(), text[index + 1 :].strip()
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _split_flow_items(text: str) -> list[str]:
|
|
84
|
+
items: list[str] = []
|
|
85
|
+
current: list[str] = []
|
|
86
|
+
in_single = False
|
|
87
|
+
in_double = False
|
|
88
|
+
escaped = False
|
|
89
|
+
depth = 0
|
|
90
|
+
for char in text:
|
|
91
|
+
if escaped:
|
|
92
|
+
current.append(char)
|
|
93
|
+
escaped = False
|
|
94
|
+
continue
|
|
95
|
+
if char == "\\" and in_double:
|
|
96
|
+
current.append(char)
|
|
97
|
+
escaped = True
|
|
98
|
+
continue
|
|
99
|
+
if char == "'" and not in_double:
|
|
100
|
+
in_single = not in_single
|
|
101
|
+
current.append(char)
|
|
102
|
+
continue
|
|
103
|
+
if char == '"' and not in_single:
|
|
104
|
+
in_double = not in_double
|
|
105
|
+
current.append(char)
|
|
106
|
+
continue
|
|
107
|
+
if char in "[{" and not in_single and not in_double:
|
|
108
|
+
depth += 1
|
|
109
|
+
current.append(char)
|
|
110
|
+
continue
|
|
111
|
+
if char in "]}" and not in_single and not in_double:
|
|
112
|
+
depth -= 1
|
|
113
|
+
current.append(char)
|
|
114
|
+
continue
|
|
115
|
+
if char == "," and not in_single and not in_double and depth == 0:
|
|
116
|
+
item = "".join(current).strip()
|
|
117
|
+
if item:
|
|
118
|
+
items.append(item)
|
|
119
|
+
current = []
|
|
120
|
+
continue
|
|
121
|
+
current.append(char)
|
|
122
|
+
tail = "".join(current).strip()
|
|
123
|
+
if tail:
|
|
124
|
+
items.append(tail)
|
|
125
|
+
return items
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _parse_scalar(text: str) -> Any:
|
|
129
|
+
if text == "":
|
|
130
|
+
return ""
|
|
131
|
+
if text[0] in {'"', "'"} and text[-1] == text[0]:
|
|
132
|
+
return text[1:-1]
|
|
133
|
+
lowered = text.lower()
|
|
134
|
+
if lowered in {"true", "yes", "on"}:
|
|
135
|
+
return True
|
|
136
|
+
if lowered in {"false", "no", "off"}:
|
|
137
|
+
return False
|
|
138
|
+
if lowered in {"null", "~"}:
|
|
139
|
+
return None
|
|
140
|
+
if text.startswith("[") and text.endswith("]"):
|
|
141
|
+
inner = text[1:-1].strip()
|
|
142
|
+
if not inner:
|
|
143
|
+
return []
|
|
144
|
+
return [_parse_scalar(item) for item in _split_flow_items(inner)]
|
|
145
|
+
if text.startswith("{") and text.endswith("}"):
|
|
146
|
+
inner = text[1:-1].strip()
|
|
147
|
+
if not inner:
|
|
148
|
+
return {}
|
|
149
|
+
out: dict[str, Any] = {}
|
|
150
|
+
for item in _split_flow_items(inner):
|
|
151
|
+
pair = _split_key_value(item)
|
|
152
|
+
if pair is None:
|
|
153
|
+
raise ValueError(f"Invalid flow mapping item: {item!r}")
|
|
154
|
+
out[str(_parse_scalar(pair[0]))] = _parse_scalar(pair[1])
|
|
155
|
+
return out
|
|
156
|
+
try:
|
|
157
|
+
if text.startswith("0") and text not in {"0", "0.0"} and not text.startswith("0."):
|
|
158
|
+
raise ValueError
|
|
159
|
+
return int(text)
|
|
160
|
+
except ValueError:
|
|
161
|
+
pass
|
|
162
|
+
try:
|
|
163
|
+
return float(text)
|
|
164
|
+
except ValueError:
|
|
165
|
+
return text
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class _Parser:
|
|
169
|
+
def __init__(self, lines: list[tuple[int, str]]):
|
|
170
|
+
self.lines = lines
|
|
171
|
+
self.index = 0
|
|
172
|
+
|
|
173
|
+
def parse(self) -> Any:
|
|
174
|
+
if not self.lines:
|
|
175
|
+
return None
|
|
176
|
+
return self._parse_block(self.lines[0][0])
|
|
177
|
+
|
|
178
|
+
def _parse_block(self, indent: int) -> Any:
|
|
179
|
+
if self.index >= len(self.lines):
|
|
180
|
+
return None
|
|
181
|
+
current_indent, content = self.lines[self.index]
|
|
182
|
+
if current_indent < indent:
|
|
183
|
+
return None
|
|
184
|
+
if content.startswith("- "):
|
|
185
|
+
return self._parse_list(indent)
|
|
186
|
+
return self._parse_mapping(indent)
|
|
187
|
+
|
|
188
|
+
def _parse_mapping(self, indent: int) -> dict[str, Any]:
|
|
189
|
+
out: dict[str, Any] = {}
|
|
190
|
+
while self.index < len(self.lines):
|
|
191
|
+
current_indent, content = self.lines[self.index]
|
|
192
|
+
if current_indent < indent:
|
|
193
|
+
break
|
|
194
|
+
if current_indent != indent or content.startswith("- "):
|
|
195
|
+
break
|
|
196
|
+
pair = _split_key_value(content)
|
|
197
|
+
if pair is None:
|
|
198
|
+
raise ValueError(f"Invalid mapping line: {content!r}")
|
|
199
|
+
raw_key, raw_value = pair
|
|
200
|
+
key = str(_parse_scalar(raw_key))
|
|
201
|
+
self.index += 1
|
|
202
|
+
if raw_value:
|
|
203
|
+
out[key] = _parse_scalar(raw_value)
|
|
204
|
+
continue
|
|
205
|
+
if self.index < len(self.lines) and self.lines[self.index][0] > current_indent:
|
|
206
|
+
out[key] = self._parse_block(self.lines[self.index][0])
|
|
207
|
+
else:
|
|
208
|
+
out[key] = None
|
|
209
|
+
return out
|
|
210
|
+
|
|
211
|
+
def _parse_list(self, indent: int) -> list[Any]:
|
|
212
|
+
out: list[Any] = []
|
|
213
|
+
while self.index < len(self.lines):
|
|
214
|
+
current_indent, content = self.lines[self.index]
|
|
215
|
+
if current_indent < indent:
|
|
216
|
+
break
|
|
217
|
+
if current_indent != indent or not content.startswith("- "):
|
|
218
|
+
break
|
|
219
|
+
item_content = content[2:].strip()
|
|
220
|
+
self.index += 1
|
|
221
|
+
if not item_content:
|
|
222
|
+
if self.index < len(self.lines) and self.lines[self.index][0] > current_indent:
|
|
223
|
+
out.append(self._parse_block(self.lines[self.index][0]))
|
|
224
|
+
else:
|
|
225
|
+
out.append(None)
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
pair = _split_key_value(item_content)
|
|
229
|
+
if pair is not None and pair[0] and " " not in pair[0]:
|
|
230
|
+
raw_key, raw_value = pair
|
|
231
|
+
item: dict[str, Any] = {str(_parse_scalar(raw_key)): _parse_scalar(raw_value) if raw_value else None}
|
|
232
|
+
if not raw_value and self.index < len(self.lines) and self.lines[self.index][0] > current_indent:
|
|
233
|
+
item[str(_parse_scalar(raw_key))] = self._parse_block(self.lines[self.index][0])
|
|
234
|
+
if self.index < len(self.lines) and self.lines[self.index][0] > current_indent:
|
|
235
|
+
extra = self._parse_block(self.lines[self.index][0])
|
|
236
|
+
if isinstance(extra, dict):
|
|
237
|
+
item.update(extra)
|
|
238
|
+
out.append(item)
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
out.append(_parse_scalar(item_content))
|
|
242
|
+
return out
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def safe_load(stream: Any) -> Any:
|
|
246
|
+
text = _read_text(stream)
|
|
247
|
+
stripped = text.strip()
|
|
248
|
+
if not stripped:
|
|
249
|
+
return None
|
|
250
|
+
if stripped[0] in {"{", "["}:
|
|
251
|
+
try:
|
|
252
|
+
return json.loads(stripped)
|
|
253
|
+
except json.JSONDecodeError:
|
|
254
|
+
pass
|
|
255
|
+
return _Parser(_prepare_lines(text)).parse()
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _dump_scalar(value: Any) -> str:
|
|
259
|
+
if value is None:
|
|
260
|
+
return "null"
|
|
261
|
+
if isinstance(value, bool):
|
|
262
|
+
return "true" if value else "false"
|
|
263
|
+
if isinstance(value, (int, float)):
|
|
264
|
+
return str(value)
|
|
265
|
+
return json.dumps(str(value), ensure_ascii=False)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _dump_lines(value: Any, indent: int) -> list[str]:
|
|
269
|
+
prefix = " " * indent
|
|
270
|
+
if isinstance(value, dict):
|
|
271
|
+
lines: list[str] = []
|
|
272
|
+
for key, item in value.items():
|
|
273
|
+
if isinstance(item, (dict, list)):
|
|
274
|
+
lines.append(f"{prefix}{key}:")
|
|
275
|
+
lines.extend(_dump_lines(item, indent + 2))
|
|
276
|
+
else:
|
|
277
|
+
lines.append(f"{prefix}{key}: {_dump_scalar(item)}")
|
|
278
|
+
return lines
|
|
279
|
+
if isinstance(value, list):
|
|
280
|
+
lines = []
|
|
281
|
+
for item in value:
|
|
282
|
+
if isinstance(item, dict):
|
|
283
|
+
nested = _dump_lines(item, indent + 2)
|
|
284
|
+
if nested:
|
|
285
|
+
first = nested[0].strip()
|
|
286
|
+
lines.append(f"{prefix}- {first}")
|
|
287
|
+
for line in nested[1:]:
|
|
288
|
+
lines.append(line)
|
|
289
|
+
else:
|
|
290
|
+
lines.append(f"{prefix}- {{}}")
|
|
291
|
+
elif isinstance(item, list):
|
|
292
|
+
lines.append(f"{prefix}-")
|
|
293
|
+
lines.extend(_dump_lines(item, indent + 2))
|
|
294
|
+
else:
|
|
295
|
+
lines.append(f"{prefix}- {_dump_scalar(item)}")
|
|
296
|
+
return lines
|
|
297
|
+
return [f"{prefix}{_dump_scalar(value)}"]
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def safe_dump(
|
|
301
|
+
data: Any,
|
|
302
|
+
stream: io.TextIOBase | None = None,
|
|
303
|
+
default_flow_style: bool | None = None,
|
|
304
|
+
sort_keys: bool = False,
|
|
305
|
+
**_: Any,
|
|
306
|
+
) -> str | None:
|
|
307
|
+
_ = default_flow_style
|
|
308
|
+
dump_data = data
|
|
309
|
+
if sort_keys and isinstance(data, dict):
|
|
310
|
+
dump_data = dict(sorted(data.items()))
|
|
311
|
+
text = "\n".join(_dump_lines(dump_data, 0)) + "\n"
|
|
312
|
+
if stream is not None:
|
|
313
|
+
stream.write(text)
|
|
314
|
+
return None
|
|
315
|
+
return text
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
dump = safe_dump
|
|
319
|
+
load = safe_load
|
|
320
|
+
|
|
321
|
+
__all__ = ["dump", "load", "safe_dump", "safe_load"]
|
|
@@ -1,26 +1,113 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "
|
|
3
|
-
allowed-tools: Read,
|
|
4
|
-
argument-hint: "[
|
|
2
|
+
description: "Analyze uncommitted git changes and propose logical atomic commits grouped by concern."
|
|
3
|
+
allowed-tools: Read, Bash(python*:*), Grep, Glob
|
|
4
|
+
argument-hint: "[optional: working directory path]"
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
# /OMG:ai-commit
|
|
7
|
+
# /OMG:ai-commit — AI Commit Splitter
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Analyze uncommitted git changes and propose logical atomic commits grouped by concern.
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Usage
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
-
|
|
13
|
+
```
|
|
14
|
+
/OMG:ai-commit
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## What It Does
|
|
18
|
+
|
|
19
|
+
1. Reads hunk-level git diffs via `git_hunk()` from `tools/git_inspector.py`
|
|
20
|
+
2. Groups hunks by file type/category (python, javascript, config, docs, tests, etc.)
|
|
21
|
+
3. Separates test files from source code automatically
|
|
22
|
+
4. Generates conventional commit messages: `{type}({scope}): {description}`
|
|
23
|
+
5. Displays a human-readable preview of proposed commits
|
|
24
|
+
|
|
25
|
+
## Feature Flag
|
|
26
|
+
|
|
27
|
+
- **Flag name**: `OMG_AI_COMMIT_ENABLED`
|
|
28
|
+
- **Default**: `False` (disabled)
|
|
29
|
+
- **Enable**: `export OMG_AI_COMMIT_ENABLED=1`
|
|
30
|
+
|
|
31
|
+
Or set in `settings.json`:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"_omg": {
|
|
36
|
+
"features": {
|
|
37
|
+
"ai_commit": true
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Output Example
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
============================================================
|
|
47
|
+
OMG AI Commit Splitter — Proposed Commit Plan
|
|
48
|
+
============================================================
|
|
49
|
+
|
|
50
|
+
Total proposed commits: 3
|
|
15
51
|
|
|
16
|
-
|
|
52
|
+
Commit 1: feat(tools): update commit_splitter.py
|
|
53
|
+
──────────────────────────────────────────────────
|
|
54
|
+
• tools/commit_splitter.py
|
|
55
|
+
(2 hunks)
|
|
56
|
+
|
|
57
|
+
Commit 2: test(tests): update 2 test files
|
|
58
|
+
──────────────────────────────────────────────────
|
|
59
|
+
• tests/tools/test_commit_splitter.py
|
|
60
|
+
• tests/tools/test_git_inspector.py
|
|
61
|
+
(4 hunks)
|
|
62
|
+
|
|
63
|
+
Commit 3: docs(commands): update ai-commit.md
|
|
64
|
+
──────────────────────────────────────────────────
|
|
65
|
+
• commands/ai-commit.md
|
|
66
|
+
(1 hunk)
|
|
67
|
+
|
|
68
|
+
============================================================
|
|
69
|
+
NOTE: This is a preview only. No commits were made.
|
|
70
|
+
============================================================
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Commit Type Mapping
|
|
74
|
+
|
|
75
|
+
| Category | Default Type | Description |
|
|
76
|
+
|------------|-------------|--------------------------|
|
|
77
|
+
| python | `feat` | Python source files |
|
|
78
|
+
| javascript | `feat` | JS/TS source files |
|
|
79
|
+
| tests | `test` | Test files (any language) |
|
|
80
|
+
| docs | `docs` | Documentation files |
|
|
81
|
+
| config | `chore` | Configuration files |
|
|
82
|
+
| shell | `chore` | Shell scripts |
|
|
83
|
+
| styles | `style` | CSS/SCSS/LESS files |
|
|
84
|
+
| markup | `feat` | HTML/XML/SVG files |
|
|
85
|
+
| other | `chore` | Unclassified files |
|
|
86
|
+
|
|
87
|
+
## CLI Usage
|
|
17
88
|
|
|
18
89
|
```bash
|
|
19
|
-
|
|
90
|
+
# Preview commit plan from terminal
|
|
91
|
+
python3 tools/commit_splitter.py --dry-run
|
|
20
92
|
```
|
|
21
93
|
|
|
22
|
-
|
|
94
|
+
## Safety
|
|
23
95
|
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
96
|
+
- **Read-only**: Never executes `git commit` or modifies the working tree
|
|
97
|
+
- **Feature-gated**: Returns empty results when disabled
|
|
98
|
+
- **Non-destructive**: Safe to run at any time
|
|
99
|
+
|
|
100
|
+
## API
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from tools.commit_splitter import analyze_changes, generate_commit_plan, preview_commit_plan
|
|
104
|
+
|
|
105
|
+
# Get grouped hunks
|
|
106
|
+
groups = analyze_changes(cwd=".")
|
|
107
|
+
|
|
108
|
+
# Get full commit plan with messages
|
|
109
|
+
plan = generate_commit_plan(cwd=".")
|
|
110
|
+
|
|
111
|
+
# Get human-readable preview string
|
|
112
|
+
preview = preview_commit_plan(cwd=".")
|
|
113
|
+
```
|