@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,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()