@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,404 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PR Description Generator for OMG
|
|
4
|
+
|
|
5
|
+
Generates structured pull request descriptions from branch name,
|
|
6
|
+
commit history, and diff statistics. Output-only tool — never
|
|
7
|
+
invokes `gh pr create` or modifies git state.
|
|
8
|
+
|
|
9
|
+
No feature flag required (documentation output, not a hook).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
# Conventional commit regex (shared pattern with changelog_generator / commit_splitter)
|
|
17
|
+
_CONVENTIONAL_RE = re.compile(
|
|
18
|
+
r"^(?P<type>[a-z]+)(?:\((?P<scope>[^)]+)\))?!?:\s*(?P<description>.+)$"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Type → human-readable category for Changes section
|
|
22
|
+
_TYPE_LABEL = {
|
|
23
|
+
"feat": "Features",
|
|
24
|
+
"fix": "Bug fixes",
|
|
25
|
+
"refactor": "Refactoring",
|
|
26
|
+
"perf": "Performance",
|
|
27
|
+
"docs": "Documentation",
|
|
28
|
+
"test": "Tests",
|
|
29
|
+
"style": "Style",
|
|
30
|
+
"chore": "Chores",
|
|
31
|
+
"ci": "CI/CD",
|
|
32
|
+
"build": "Build",
|
|
33
|
+
"sec": "Security",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Branch prefix → PR intent hint
|
|
37
|
+
_BRANCH_INTENT = {
|
|
38
|
+
"feature": "Add",
|
|
39
|
+
"feat": "Add",
|
|
40
|
+
"fix": "Fix",
|
|
41
|
+
"bugfix": "Fix",
|
|
42
|
+
"hotfix": "Hotfix",
|
|
43
|
+
"refactor": "Refactor",
|
|
44
|
+
"docs": "Document",
|
|
45
|
+
"chore": "Maintain",
|
|
46
|
+
"ci": "Update CI for",
|
|
47
|
+
"release": "Release",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# Internal helpers
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
def _parse_branch(branch_name: str) -> Dict[str, str]:
|
|
56
|
+
"""Extract prefix and slug from branch name.
|
|
57
|
+
|
|
58
|
+
Examples:
|
|
59
|
+
"feature/add-jwt-auth" -> {"prefix": "feature", "slug": "add-jwt-auth"}
|
|
60
|
+
"fix/null-pointer" -> {"prefix": "fix", "slug": "null-pointer"}
|
|
61
|
+
"main" -> {"prefix": "", "slug": "main"}
|
|
62
|
+
"""
|
|
63
|
+
if "/" in branch_name:
|
|
64
|
+
prefix, slug = branch_name.split("/", 1)
|
|
65
|
+
return {"prefix": prefix, "slug": slug}
|
|
66
|
+
return {"prefix": "", "slug": branch_name}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _slug_to_words(slug: str) -> str:
|
|
70
|
+
"""Convert a branch slug to human-readable words.
|
|
71
|
+
|
|
72
|
+
"add-jwt-auth" -> "add jwt auth"
|
|
73
|
+
"null_pointer_fix" -> "null pointer fix"
|
|
74
|
+
"""
|
|
75
|
+
return slug.replace("-", " ").replace("_", " ").strip()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _parse_commit(message: str) -> Dict[str, str]:
|
|
79
|
+
"""Parse a conventional commit message into components.
|
|
80
|
+
|
|
81
|
+
Returns dict with keys: type, scope, description.
|
|
82
|
+
Non-conventional messages get type="" and the full message as description.
|
|
83
|
+
"""
|
|
84
|
+
match = _CONVENTIONAL_RE.match(message.strip())
|
|
85
|
+
if match:
|
|
86
|
+
return {
|
|
87
|
+
"type": match.group("type").lower(),
|
|
88
|
+
"scope": match.group("scope") or "",
|
|
89
|
+
"description": match.group("description").strip(),
|
|
90
|
+
}
|
|
91
|
+
return {"type": "", "scope": "", "description": message.strip()}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _group_commits_by_type(
|
|
95
|
+
commits: List[Dict[str, Any]],
|
|
96
|
+
) -> Dict[str, List[Dict[str, str]]]:
|
|
97
|
+
"""Group parsed commits by their conventional type.
|
|
98
|
+
|
|
99
|
+
Returns dict mapping type label -> list of parsed commits.
|
|
100
|
+
Non-conventional commits go under "Other".
|
|
101
|
+
"""
|
|
102
|
+
groups: Dict[str, List[Dict[str, str]]] = {}
|
|
103
|
+
for commit in commits:
|
|
104
|
+
message = commit.get("message", "").strip()
|
|
105
|
+
if not message:
|
|
106
|
+
continue
|
|
107
|
+
parsed = _parse_commit(message)
|
|
108
|
+
commit_type = parsed["type"]
|
|
109
|
+
label = _TYPE_LABEL.get(commit_type, "Other")
|
|
110
|
+
groups.setdefault(label, []).append(parsed)
|
|
111
|
+
return groups
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _group_commits_by_scope(
|
|
115
|
+
commits: List[Dict[str, Any]],
|
|
116
|
+
) -> Dict[str, List[str]]:
|
|
117
|
+
"""Group commit descriptions by scope for the Changes section.
|
|
118
|
+
|
|
119
|
+
Returns dict mapping scope -> list of descriptions.
|
|
120
|
+
Commits without scope are grouped under their type label.
|
|
121
|
+
"""
|
|
122
|
+
groups: Dict[str, List[str]] = {}
|
|
123
|
+
for commit in commits:
|
|
124
|
+
message = commit.get("message", "").strip()
|
|
125
|
+
if not message:
|
|
126
|
+
continue
|
|
127
|
+
parsed = _parse_commit(message)
|
|
128
|
+
scope = parsed["scope"]
|
|
129
|
+
if not scope:
|
|
130
|
+
# Use type label or "general" as fallback scope
|
|
131
|
+
scope = _TYPE_LABEL.get(parsed["type"], "general")
|
|
132
|
+
groups.setdefault(scope, []).append(parsed["description"])
|
|
133
|
+
return groups
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
# Summary generation
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
def _generate_summary(
|
|
141
|
+
branch_name: str,
|
|
142
|
+
commits: List[Dict[str, Any]],
|
|
143
|
+
diff_stats: Dict[str, Any],
|
|
144
|
+
) -> List[str]:
|
|
145
|
+
"""Generate 1-3 bullet points summarizing the PR.
|
|
146
|
+
|
|
147
|
+
Strategy:
|
|
148
|
+
1. Derive intent from branch prefix + slug.
|
|
149
|
+
2. Pull key commit descriptions (feat/fix first).
|
|
150
|
+
3. Add diff stats context.
|
|
151
|
+
"""
|
|
152
|
+
lines: List[str] = []
|
|
153
|
+
branch_info = _parse_branch(branch_name)
|
|
154
|
+
intent = _BRANCH_INTENT.get(branch_info["prefix"], "")
|
|
155
|
+
slug_words = _slug_to_words(branch_info["slug"])
|
|
156
|
+
|
|
157
|
+
# Collect feature/fix descriptions for summary bullets
|
|
158
|
+
feat_descs: List[str] = []
|
|
159
|
+
fix_descs: List[str] = []
|
|
160
|
+
other_descs: List[str] = []
|
|
161
|
+
|
|
162
|
+
for commit in commits:
|
|
163
|
+
message = commit.get("message", "").strip()
|
|
164
|
+
if not message:
|
|
165
|
+
continue
|
|
166
|
+
parsed = _parse_commit(message)
|
|
167
|
+
if parsed["type"] == "feat":
|
|
168
|
+
feat_descs.append(parsed["description"])
|
|
169
|
+
elif parsed["type"] == "fix":
|
|
170
|
+
fix_descs.append(parsed["description"])
|
|
171
|
+
elif parsed["type"] not in ("test", "docs", "style", "ci", "chore"):
|
|
172
|
+
other_descs.append(parsed["description"])
|
|
173
|
+
|
|
174
|
+
# Build bullets: prioritize feat, then fix, then other
|
|
175
|
+
all_descs = feat_descs + fix_descs + other_descs
|
|
176
|
+
|
|
177
|
+
if all_descs:
|
|
178
|
+
# Use up to 3 unique descriptions
|
|
179
|
+
seen = set()
|
|
180
|
+
for desc in all_descs:
|
|
181
|
+
if desc not in seen and len(lines) < 3:
|
|
182
|
+
# Capitalize first letter
|
|
183
|
+
bullet = desc[0].upper() + desc[1:] if len(desc) > 1 else desc.upper()
|
|
184
|
+
lines.append(f"- {bullet}")
|
|
185
|
+
seen.add(desc)
|
|
186
|
+
elif slug_words:
|
|
187
|
+
# Fallback: derive from branch name
|
|
188
|
+
prefix = f"{intent} " if intent else ""
|
|
189
|
+
lines.append(f"- {prefix}{slug_words}")
|
|
190
|
+
|
|
191
|
+
# If we still have no bullets, add a generic one
|
|
192
|
+
if not lines:
|
|
193
|
+
lines.append("- Update project")
|
|
194
|
+
|
|
195
|
+
# Add diff stats bullet if meaningful
|
|
196
|
+
files_changed = diff_stats.get("files_changed", 0)
|
|
197
|
+
insertions = diff_stats.get("insertions", 0)
|
|
198
|
+
deletions = diff_stats.get("deletions", 0)
|
|
199
|
+
if files_changed > 0:
|
|
200
|
+
lines.append(
|
|
201
|
+
f"- {files_changed} file{'s' if files_changed != 1 else ''} changed"
|
|
202
|
+
f" (+{insertions}, -{deletions})"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
return lines
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ---------------------------------------------------------------------------
|
|
209
|
+
# Changes generation
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
def _generate_changes(commits: List[Dict[str, Any]]) -> List[str]:
|
|
213
|
+
"""Generate the Changes section listing changes by scope/category."""
|
|
214
|
+
lines: List[str] = []
|
|
215
|
+
scope_groups = _group_commits_by_scope(commits)
|
|
216
|
+
|
|
217
|
+
if not scope_groups:
|
|
218
|
+
lines.append("- No changes recorded")
|
|
219
|
+
return lines
|
|
220
|
+
|
|
221
|
+
for scope, descriptions in sorted(scope_groups.items()):
|
|
222
|
+
# Summarize descriptions for this scope
|
|
223
|
+
unique_descs = []
|
|
224
|
+
seen = set()
|
|
225
|
+
for d in descriptions:
|
|
226
|
+
if d not in seen:
|
|
227
|
+
unique_descs.append(d)
|
|
228
|
+
seen.add(d)
|
|
229
|
+
|
|
230
|
+
if len(unique_descs) == 1:
|
|
231
|
+
lines.append(f"- **{scope}**: {unique_descs[0]}")
|
|
232
|
+
else:
|
|
233
|
+
# Combine descriptions
|
|
234
|
+
combined = "; ".join(unique_descs[:3])
|
|
235
|
+
if len(unique_descs) > 3:
|
|
236
|
+
combined += f" (+{len(unique_descs) - 3} more)"
|
|
237
|
+
lines.append(f"- **{scope}**: {combined}")
|
|
238
|
+
|
|
239
|
+
return lines
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ---------------------------------------------------------------------------
|
|
243
|
+
# Testing generation
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
def _generate_testing(commits: List[Dict[str, Any]]) -> List[str]:
|
|
247
|
+
"""Generate the Testing section from test-related commits."""
|
|
248
|
+
lines: List[str] = []
|
|
249
|
+
test_descs: List[str] = []
|
|
250
|
+
|
|
251
|
+
for commit in commits:
|
|
252
|
+
message = commit.get("message", "").strip()
|
|
253
|
+
if not message:
|
|
254
|
+
continue
|
|
255
|
+
parsed = _parse_commit(message)
|
|
256
|
+
if parsed["type"] == "test":
|
|
257
|
+
test_descs.append(parsed["description"])
|
|
258
|
+
|
|
259
|
+
if test_descs:
|
|
260
|
+
seen = set()
|
|
261
|
+
for desc in test_descs:
|
|
262
|
+
if desc not in seen:
|
|
263
|
+
bullet = desc[0].upper() + desc[1:] if len(desc) > 1 else desc.upper()
|
|
264
|
+
lines.append(f"- {bullet}")
|
|
265
|
+
seen.add(desc)
|
|
266
|
+
else:
|
|
267
|
+
lines.append("- No test changes in this PR")
|
|
268
|
+
|
|
269
|
+
return lines
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# ---------------------------------------------------------------------------
|
|
273
|
+
# Checklist generation
|
|
274
|
+
# ---------------------------------------------------------------------------
|
|
275
|
+
|
|
276
|
+
_CHECKLIST_ITEMS = [
|
|
277
|
+
"- [ ] Tests pass",
|
|
278
|
+
"- [ ] No breaking changes",
|
|
279
|
+
"- [ ] Documentation updated",
|
|
280
|
+
]
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _generate_checklist() -> List[str]:
|
|
284
|
+
"""Return standard PR checklist items."""
|
|
285
|
+
return list(_CHECKLIST_ITEMS)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# ---------------------------------------------------------------------------
|
|
289
|
+
# Public API
|
|
290
|
+
# ---------------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
def generate_pr_description(
|
|
293
|
+
branch_name: str,
|
|
294
|
+
commits: List[Dict[str, Any]],
|
|
295
|
+
diff_stats: Dict[str, Any],
|
|
296
|
+
) -> str:
|
|
297
|
+
"""Generate a structured PR description in markdown.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
branch_name: Git branch name (e.g., "feature/add-jwt-auth").
|
|
301
|
+
commits: List of dicts with ``message`` (str) and optional ``hash`` (str).
|
|
302
|
+
diff_stats: Dict with optional ``files_changed`` (int),
|
|
303
|
+
``insertions`` (int), ``deletions`` (int).
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
Formatted markdown string with sections:
|
|
307
|
+
Summary, Changes, Testing, Checklist.
|
|
308
|
+
"""
|
|
309
|
+
if not isinstance(commits, list):
|
|
310
|
+
commits = []
|
|
311
|
+
if not isinstance(diff_stats, dict):
|
|
312
|
+
diff_stats = {}
|
|
313
|
+
|
|
314
|
+
sections: List[str] = []
|
|
315
|
+
|
|
316
|
+
# Summary
|
|
317
|
+
sections.append("## Summary")
|
|
318
|
+
sections.extend(_generate_summary(branch_name, commits, diff_stats))
|
|
319
|
+
sections.append("")
|
|
320
|
+
|
|
321
|
+
# Changes
|
|
322
|
+
sections.append("## Changes")
|
|
323
|
+
sections.extend(_generate_changes(commits))
|
|
324
|
+
sections.append("")
|
|
325
|
+
|
|
326
|
+
# Testing
|
|
327
|
+
sections.append("## Testing")
|
|
328
|
+
sections.extend(_generate_testing(commits))
|
|
329
|
+
sections.append("")
|
|
330
|
+
|
|
331
|
+
# Checklist
|
|
332
|
+
sections.append("## Checklist")
|
|
333
|
+
sections.extend(_generate_checklist())
|
|
334
|
+
|
|
335
|
+
return "\n".join(sections)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def write_pr_description(
|
|
339
|
+
branch_name: str,
|
|
340
|
+
commits: List[Dict[str, Any]],
|
|
341
|
+
diff_stats: Dict[str, Any],
|
|
342
|
+
output_path: Optional[str] = None,
|
|
343
|
+
) -> str:
|
|
344
|
+
"""Generate PR description; write to *output_path* when given, else return string.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
branch_name: Git branch name.
|
|
348
|
+
commits: List of commit dicts.
|
|
349
|
+
diff_stats: Diff statistics dict.
|
|
350
|
+
output_path: File path to write. If None, returns string only.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
The generated PR description string (always returned, even when writing to file).
|
|
354
|
+
"""
|
|
355
|
+
content = generate_pr_description(branch_name, commits, diff_stats)
|
|
356
|
+
if not content:
|
|
357
|
+
return ""
|
|
358
|
+
|
|
359
|
+
if output_path is not None:
|
|
360
|
+
parent = os.path.dirname(output_path)
|
|
361
|
+
if parent:
|
|
362
|
+
os.makedirs(parent, exist_ok=True)
|
|
363
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
364
|
+
f.write(content)
|
|
365
|
+
|
|
366
|
+
return content
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# ---------------------------------------------------------------------------
|
|
370
|
+
# CLI entry point (dry-run only)
|
|
371
|
+
# ---------------------------------------------------------------------------
|
|
372
|
+
|
|
373
|
+
def main():
|
|
374
|
+
"""CLI entry point — prints PR description to stdout (dry-run)."""
|
|
375
|
+
import argparse
|
|
376
|
+
|
|
377
|
+
parser = argparse.ArgumentParser(
|
|
378
|
+
description="OMG PR Description Generator — generates structured PR markdown"
|
|
379
|
+
)
|
|
380
|
+
parser.add_argument(
|
|
381
|
+
"--branch", default="feature/unknown",
|
|
382
|
+
help="Branch name (default: feature/unknown)",
|
|
383
|
+
)
|
|
384
|
+
parser.add_argument(
|
|
385
|
+
"--output", default=None,
|
|
386
|
+
help="Output file path (default: stdout)",
|
|
387
|
+
)
|
|
388
|
+
args = parser.parse_args()
|
|
389
|
+
|
|
390
|
+
# Minimal demo with empty data
|
|
391
|
+
commits: List[Dict[str, Any]] = []
|
|
392
|
+
diff_stats: Dict[str, Any] = {}
|
|
393
|
+
|
|
394
|
+
result = write_pr_description(
|
|
395
|
+
args.branch, commits, diff_stats, output_path=args.output,
|
|
396
|
+
)
|
|
397
|
+
if args.output is None:
|
|
398
|
+
print(result)
|
|
399
|
+
else:
|
|
400
|
+
print(f"[OMG PR] Description written to {args.output}")
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
if __name__ == "__main__":
|
|
404
|
+
main()
|