@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,297 @@
1
+ #!/usr/bin/env python3
2
+ """OMG v1 Shadow Manager
3
+
4
+ Maintains overlay-style shadow writes and evidence artifacts.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import hashlib
9
+ import json
10
+ import os
11
+ import shutil
12
+ import sys
13
+ from datetime import datetime, timezone
14
+ from typing import Any
15
+
16
+ HOOKS_DIR = os.path.dirname(__file__)
17
+ if HOOKS_DIR not in sys.path:
18
+ sys.path.insert(0, HOOKS_DIR)
19
+ from _common import _resolve_project_dir
20
+
21
+
22
+ def _project_dir() -> str:
23
+ return _resolve_project_dir()
24
+
25
+
26
+ def _shadow_root(project_dir: str) -> str:
27
+ return os.path.join(project_dir, ".omg", "shadow")
28
+
29
+
30
+ def _evidence_root(project_dir: str) -> str:
31
+ return os.path.join(project_dir, ".omg", "evidence")
32
+
33
+
34
+ def _active_run_path(project_dir: str) -> str:
35
+ return os.path.join(_shadow_root(project_dir), "active-run")
36
+
37
+
38
+ def _utc_now() -> str:
39
+ return datetime.now(timezone.utc).isoformat()
40
+
41
+
42
+ def _new_run_id() -> str:
43
+ return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
44
+
45
+
46
+ def _hash_file(path: str) -> str:
47
+ h = hashlib.sha256()
48
+ with open(path, "rb") as f:
49
+ while True:
50
+ chunk = f.read(8192)
51
+ if not chunk:
52
+ break
53
+ h.update(chunk)
54
+ return h.hexdigest()
55
+
56
+
57
+ def ensure_shadow_dirs(project_dir: str) -> None:
58
+ os.makedirs(_shadow_root(project_dir), exist_ok=True)
59
+ os.makedirs(_evidence_root(project_dir), exist_ok=True)
60
+
61
+
62
+ def set_active_run_id(project_dir: str, run_id: str) -> None:
63
+ ensure_shadow_dirs(project_dir)
64
+ with open(_active_run_path(project_dir), "w", encoding="utf-8") as f:
65
+ f.write(run_id)
66
+
67
+
68
+ def get_active_run_id(project_dir: str) -> str | None:
69
+ env_id = os.environ.get("OMG_RUN_ID")
70
+ if env_id:
71
+ return env_id
72
+ path = _active_run_path(project_dir)
73
+ if not os.path.exists(path):
74
+ return None
75
+ try:
76
+ with open(path, "r", encoding="utf-8") as f:
77
+ value = f.read().strip()
78
+ return value or None
79
+ except Exception:
80
+ return None
81
+
82
+
83
+ def begin_shadow_run(project_dir: str, metadata: dict[str, Any] | None = None) -> str:
84
+ run_id = get_active_run_id(project_dir) or _new_run_id()
85
+ run_dir = os.path.join(_shadow_root(project_dir), run_id)
86
+ os.makedirs(run_dir, exist_ok=True)
87
+
88
+ manifest_path = os.path.join(run_dir, "manifest.json")
89
+ if not os.path.exists(manifest_path):
90
+ manifest = {
91
+ "run_id": run_id,
92
+ "created_at": _utc_now(),
93
+ "status": "open",
94
+ "files": [],
95
+ "metadata": metadata or {},
96
+ }
97
+ with open(manifest_path, "w", encoding="utf-8") as f:
98
+ json.dump(manifest, f, indent=2)
99
+
100
+ set_active_run_id(project_dir, run_id)
101
+ return run_id
102
+
103
+
104
+ def _load_manifest(run_dir: str) -> dict[str, Any]:
105
+ path = os.path.join(run_dir, "manifest.json")
106
+ if not os.path.exists(path):
107
+ return {"files": []}
108
+ try:
109
+ with open(path, "r", encoding="utf-8") as f:
110
+ data = json.load(f)
111
+ if isinstance(data, dict):
112
+ data.setdefault("files", [])
113
+ return data
114
+ return {"files": []}
115
+ except Exception:
116
+ return {"files": []}
117
+
118
+
119
+ def _save_manifest(run_dir: str, manifest: dict[str, Any]) -> None:
120
+ path = os.path.join(run_dir, "manifest.json")
121
+ manifest["updated_at"] = _utc_now()
122
+ with open(path, "w", encoding="utf-8") as f:
123
+ json.dump(manifest, f, indent=2)
124
+
125
+
126
+ def map_shadow_path(project_dir: str, run_id: str, file_path: str) -> str:
127
+ rel = os.path.relpath(os.path.abspath(file_path), os.path.abspath(project_dir))
128
+ rel = rel.replace("..", "_up_")
129
+ return os.path.join(_shadow_root(project_dir), run_id, "overlay", rel)
130
+
131
+
132
+ def record_shadow_write(project_dir: str, run_id: str, file_path: str, source: str = "tool") -> dict[str, Any]:
133
+ run_dir = os.path.join(_shadow_root(project_dir), run_id)
134
+ os.makedirs(run_dir, exist_ok=True)
135
+
136
+ shadow_path = map_shadow_path(project_dir, run_id, file_path)
137
+ os.makedirs(os.path.dirname(shadow_path), exist_ok=True)
138
+
139
+ abs_file = file_path if os.path.isabs(file_path) else os.path.join(project_dir, file_path)
140
+ if os.path.exists(abs_file):
141
+ shutil.copy2(abs_file, shadow_path)
142
+ file_hash = _hash_file(abs_file)
143
+ else:
144
+ file_hash = ""
145
+
146
+ manifest = _load_manifest(run_dir)
147
+ rel = os.path.relpath(abs_file, project_dir)
148
+ entry = {
149
+ "file": rel,
150
+ "shadow_file": os.path.relpath(shadow_path, run_dir),
151
+ "recorded_at": _utc_now(),
152
+ "source": source,
153
+ "sha256": file_hash,
154
+ }
155
+
156
+ files = manifest.get("files", [])
157
+ # Replace existing entry for same file.
158
+ files = [f for f in files if f.get("file") != rel]
159
+ files.append(entry)
160
+ manifest["files"] = files
161
+ _save_manifest(run_dir, manifest)
162
+
163
+ return entry
164
+
165
+
166
+ def create_evidence_pack(
167
+ project_dir: str,
168
+ run_id: str,
169
+ tests: list[dict[str, Any]] | None = None,
170
+ security_scans: list[dict[str, Any]] | None = None,
171
+ diff_summary: dict[str, Any] | None = None,
172
+ reproducibility: dict[str, Any] | None = None,
173
+ unresolved_risks: list[str] | None = None,
174
+ ) -> str:
175
+ ensure_shadow_dirs(project_dir)
176
+ evidence = {
177
+ "schema": "EvidencePack",
178
+ "run_id": run_id,
179
+ "created_at": _utc_now(),
180
+ "tests": tests or [],
181
+ "security_scans": security_scans or [],
182
+ "diff_summary": diff_summary or {},
183
+ "reproducibility": reproducibility or {},
184
+ "unresolved_risks": unresolved_risks or [],
185
+ }
186
+ evidence_path = os.path.join(_evidence_root(project_dir), f"{run_id}.json")
187
+ with open(evidence_path, "w", encoding="utf-8") as f:
188
+ json.dump(evidence, f, indent=2)
189
+ return evidence_path
190
+
191
+
192
+ def has_recent_evidence(project_dir: str, hours: int = 24) -> bool:
193
+ ev_dir = _evidence_root(project_dir)
194
+ if not os.path.isdir(ev_dir):
195
+ return False
196
+
197
+ now = datetime.now(timezone.utc).timestamp()
198
+ max_age = hours * 3600
199
+
200
+ for name in os.listdir(ev_dir):
201
+ if not name.endswith(".json"):
202
+ continue
203
+ path = os.path.join(ev_dir, name)
204
+ try:
205
+ age = now - os.path.getmtime(path)
206
+ if age <= max_age:
207
+ return True
208
+ except OSError:
209
+ continue
210
+ return False
211
+
212
+
213
+ def apply_shadow(project_dir: str, run_id: str) -> dict[str, Any]:
214
+ run_dir = os.path.join(_shadow_root(project_dir), run_id)
215
+ manifest = _load_manifest(run_dir)
216
+ applied = []
217
+
218
+ for item in manifest.get("files", []):
219
+ rel = item.get("file")
220
+ shadow_rel = item.get("shadow_file")
221
+ if not rel or not shadow_rel:
222
+ continue
223
+ src = os.path.join(run_dir, shadow_rel)
224
+ dst = os.path.join(project_dir, rel)
225
+ if not os.path.exists(src):
226
+ continue
227
+ os.makedirs(os.path.dirname(dst), exist_ok=True)
228
+ shutil.copy2(src, dst)
229
+ applied.append(rel)
230
+
231
+ manifest["status"] = "applied"
232
+ manifest["applied_at"] = _utc_now()
233
+ _save_manifest(run_dir, manifest)
234
+
235
+ return {"run_id": run_id, "applied": applied}
236
+
237
+
238
+ def drop_shadow(project_dir: str, run_id: str) -> dict[str, Any]:
239
+ run_dir = os.path.join(_shadow_root(project_dir), run_id)
240
+ if os.path.isdir(run_dir):
241
+ print(f"[OMG] Deleting: {run_dir}", file=sys.stderr)
242
+ shutil.rmtree(run_dir, ignore_errors=True)
243
+
244
+ active = get_active_run_id(project_dir)
245
+ if active == run_id:
246
+ try:
247
+ os.remove(_active_run_path(project_dir))
248
+ except OSError:
249
+ pass
250
+
251
+ return {"run_id": run_id, "dropped": True}
252
+
253
+
254
+ def _handle_post_tool_use(payload: dict[str, Any]) -> None:
255
+ tool = payload.get("tool_name", "")
256
+ if tool not in ("Write", "Edit", "MultiEdit"):
257
+ return
258
+
259
+ tool_input = payload.get("tool_input", {}) if isinstance(payload.get("tool_input"), dict) else {}
260
+ file_path = tool_input.get("file_path", "")
261
+ if not file_path:
262
+ return
263
+
264
+ tool_resp = payload.get("tool_response", {})
265
+ success = None
266
+ if isinstance(tool_resp, dict):
267
+ success = tool_resp.get("success")
268
+
269
+ if success is False:
270
+ return
271
+
272
+ project_dir = _project_dir()
273
+ run_id = begin_shadow_run(project_dir, metadata={"source": "post-tool-use"})
274
+ record_shadow_write(project_dir, run_id, file_path)
275
+
276
+
277
+ def _main() -> int:
278
+ # Early-exit: skip all work if shadow/evidence mode is not enabled
279
+ project_dir = _project_dir()
280
+ policy_path = os.path.join(project_dir, ".omg", "policy.yaml")
281
+ if not os.path.exists(policy_path) and os.environ.get("OMG_EVIDENCE_REQUIRED", "0") != "1":
282
+ return 0
283
+
284
+ try:
285
+ payload = json.load(sys.stdin)
286
+ except Exception:
287
+ return 0
288
+
289
+ try:
290
+ _handle_post_tool_use(payload)
291
+ except Exception:
292
+ pass
293
+ return 0
294
+
295
+
296
+ if __name__ == "__main__":
297
+ raise SystemExit(_main())
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env python3
2
+ """State path resolution and legacy `.omc` -> `.omg` migration utilities."""
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+ import hashlib
7
+ import json
8
+ import os
9
+ import shutil
10
+ from typing import Any
11
+
12
+ DEFAULT_MIGRATION_REPORT = "legacy-to-omg.json"
13
+ LEGACY_MIGRATION_REPORT = "omc-to-omg.json"
14
+
15
+
16
+ def paths(project_dir: str) -> dict[str, str]:
17
+ omg_root = os.path.join(project_dir, ".omg")
18
+ return {
19
+ "project": project_dir,
20
+ "omg_root": omg_root,
21
+ "omg_state": os.path.join(omg_root, "state"),
22
+ "omg_knowledge": os.path.join(omg_root, "knowledge"),
23
+ "omg_migrations": os.path.join(omg_root, "migrations"),
24
+ "legacy_omc": os.path.join(project_dir, ".omc"),
25
+ }
26
+
27
+
28
+ def ensure_omg_structure(project_dir: str) -> None:
29
+ p = paths(project_dir)
30
+ os.makedirs(p["omg_state"], exist_ok=True)
31
+ os.makedirs(p["omg_knowledge"], exist_ok=True)
32
+ os.makedirs(p["omg_migrations"], exist_ok=True)
33
+
34
+
35
+ def _sha256_file(path: str) -> str:
36
+ h = hashlib.sha256()
37
+ with open(path, "rb") as f:
38
+ while True:
39
+ chunk = f.read(8192)
40
+ if not chunk:
41
+ break
42
+ h.update(chunk)
43
+ return h.hexdigest()
44
+
45
+
46
+ def _copy_file(src: str, dst: str) -> str:
47
+ os.makedirs(os.path.dirname(dst), exist_ok=True)
48
+ if os.path.exists(dst):
49
+ try:
50
+ if _sha256_file(src) == _sha256_file(dst):
51
+ return "unchanged"
52
+ except Exception:
53
+ pass
54
+ shutil.copy2(src, dst)
55
+ return "copied"
56
+
57
+
58
+ def _copy_tree(src: str, dst: str) -> dict[str, int]:
59
+ stats = {"copied": 0, "unchanged": 0, "missing": 0, "errors": 0}
60
+ if not os.path.isdir(src):
61
+ stats["missing"] += 1
62
+ return stats
63
+ for root, _, files in os.walk(src):
64
+ for fn in files:
65
+ src_file = os.path.join(root, fn)
66
+ rel = os.path.relpath(src_file, src)
67
+ dst_file = os.path.join(dst, rel)
68
+ try:
69
+ status = _copy_file(src_file, dst_file)
70
+ stats[status] += 1
71
+ except Exception:
72
+ stats["errors"] += 1
73
+ return stats
74
+
75
+
76
+ MIGRATION_MAP: list[tuple[str, str, str]] = [
77
+ ("file", "profile.yaml", "state/profile.yaml"),
78
+ ("file", "working-memory.md", "state/working-memory.md"),
79
+ ("file", "quality-gate.json", "state/quality-gate.json"),
80
+ ("file", "_plan.md", "state/_plan.md"),
81
+ ("file", "_checklist.md", "state/_checklist.md"),
82
+ ("file", "_context.md", "state/_context.md"),
83
+ ("file", "handoff.md", "state/handoff.md"),
84
+ ("file", "handoff.md.consumed", "state/handoff.md.consumed"),
85
+ ("file", "handoff-portable.md", "state/handoff-portable.md"),
86
+ # Legacy root-level runtime files used by older OMC installs.
87
+ ("file", "autopilot-state.json", "state/autopilot-state.json"),
88
+ ("file", "ultrapilot-state.json", "state/ultrapilot-state.json"),
89
+ ("file", "ralph-state.json", "state/ralph-state.json"),
90
+ ("file", "ultrawork-state.json", "state/ultrawork-state.json"),
91
+ ("file", "ultraqa-state.json", "state/ultraqa-state.json"),
92
+ ("file", "team-state.json", "state/team-state.json"),
93
+ ("file", "pipeline-state.json", "state/pipeline-state.json"),
94
+ ("file", "swarm-state.json", "state/swarm-state.json"),
95
+ ("file", "hud-state.json", "state/hud-state.json"),
96
+ ("file", "hud-stdin-cache.json", "state/hud-stdin-cache.json"),
97
+ ("file", "skill-active-state.json", "state/skill-active-state.json"),
98
+ ("file", "subagent-tracking.json", "state/subagent-tracking.json"),
99
+ # Runtime state files for HUD, routing, and session continuity.
100
+ ("file", "ecosystem-lock.json", "state/ecosystem-lock.json"),
101
+ ("file", "hashline_cache.json", "state/hashline_cache.json"),
102
+ ("file", "hashline_cache.json.tmp", "state/hashline_cache.json.tmp"),
103
+ ("file", "idle_signal.json", "state/idle_signal.json"),
104
+ ("file", "idle-notif-cooldown.json", "state/idle-notif-cooldown.json"),
105
+ ("file", "last-tool-error.json", "state/last-tool-error.json"),
106
+ ("file", "routing_result.json", "state/routing_result.json"),
107
+ ("file", "theme.json", "state/theme.json"),
108
+ ("file", "todo_progress.json", "state/todo_progress.json"),
109
+ # Migrate full runtime state for HUD + routing continuity.
110
+ ("dir", "state", "state"),
111
+ ("dir", "checkpoints", "state/checkpoints"),
112
+ ("dir", "ledger", "state/ledger"),
113
+ ("dir", "learnings", "state/learnings"),
114
+ ("dir", "memory", "state/memory"),
115
+ ("dir", "repl_sessions", "state/repl_sessions"),
116
+ ("dir", "sessions", "state/sessions"),
117
+ ("dir", "snapshots", "state/snapshots"),
118
+ ("dir", "knowledge", "knowledge"),
119
+ ]
120
+
121
+
122
+ def migrate_omc_to_omg(project_dir: str, force: bool = False) -> dict[str, Any]:
123
+ ensure_omg_structure(project_dir)
124
+ p = paths(project_dir)
125
+ legacy = p["legacy_omc"]
126
+ report: dict[str, Any] = {
127
+ "started_at": datetime.now(timezone.utc).isoformat(),
128
+ "project_dir": project_dir,
129
+ "legacy_path": legacy,
130
+ "target_path": p["omg_root"],
131
+ "result": "ok",
132
+ "entries": [],
133
+ }
134
+
135
+ if not os.path.isdir(legacy):
136
+ report["result"] = "no_legacy"
137
+ else:
138
+ for kind, src_rel, dst_rel in MIGRATION_MAP:
139
+ src = os.path.join(legacy, src_rel)
140
+ dst = os.path.join(p["omg_root"], dst_rel)
141
+ entry = {"kind": kind, "source": src_rel, "target": dst_rel, "status": "missing"}
142
+ try:
143
+ if kind == "file":
144
+ if os.path.isfile(src):
145
+ status = _copy_file(src, dst)
146
+ entry["status"] = status
147
+ else:
148
+ entry["status"] = "missing"
149
+ else:
150
+ stats = _copy_tree(src, dst)
151
+ entry["status"] = "copied" if stats["copied"] else ("unchanged" if stats["unchanged"] else "missing")
152
+ entry["stats"] = stats
153
+ except Exception as exc:
154
+ entry["status"] = "error"
155
+ entry["error"] = str(exc)
156
+ report["result"] = "partial_error"
157
+ report["entries"].append(entry)
158
+
159
+ report["finished_at"] = datetime.now(timezone.utc).isoformat()
160
+ os.makedirs(p["omg_migrations"], exist_ok=True)
161
+ out_path = os.path.join(p["omg_migrations"], DEFAULT_MIGRATION_REPORT)
162
+ legacy_out_path = os.path.join(p["omg_migrations"], LEGACY_MIGRATION_REPORT)
163
+
164
+ # idempotent write: overwrite with latest summary
165
+ for path in (out_path, legacy_out_path):
166
+ with open(path, "w", encoding="utf-8") as f:
167
+ json.dump(report, f, indent=2, ensure_ascii=True)
168
+ report["report_path"] = out_path
169
+ report["legacy_report_path"] = legacy_out_path
170
+ return report
171
+
172
+
173
+ def migrate_legacy_to_omg(project_dir: str, force: bool = False) -> dict[str, Any]:
174
+ """Canonical migration API."""
175
+ return migrate_omc_to_omg(project_dir, force=force)
176
+
177
+
178
+ def resolve_state_file(
179
+ project_dir: str,
180
+ omg_relative: str,
181
+ legacy_relative: str | None = None,
182
+ auto_migrate: bool = True,
183
+ ) -> str:
184
+ """Return preferred .omg file path; fallback to .omc if needed.
185
+
186
+ - If .omg target exists, return it.
187
+ - If not, and legacy exists, optionally run migration and return target if created.
188
+ - If still absent, return the preferred target path.
189
+ """
190
+ p = paths(project_dir)
191
+ preferred = os.path.join(p["omg_root"], omg_relative)
192
+ if os.path.exists(preferred):
193
+ return preferred
194
+
195
+ legacy_rel = legacy_relative or omg_relative
196
+ legacy_path = os.path.join(p["legacy_omc"], legacy_rel)
197
+ if os.path.exists(legacy_path) and auto_migrate:
198
+ migrate_legacy_to_omg(project_dir)
199
+ if os.path.exists(preferred):
200
+ return preferred
201
+ if os.path.exists(legacy_path):
202
+ return legacy_path
203
+ return preferred
204
+
205
+
206
+ def resolve_state_dir(
207
+ project_dir: str,
208
+ omg_relative: str,
209
+ legacy_relative: str | None = None,
210
+ auto_migrate: bool = True,
211
+ ) -> str:
212
+ p = paths(project_dir)
213
+ preferred = os.path.join(p["omg_root"], omg_relative)
214
+ if os.path.isdir(preferred):
215
+ return preferred
216
+
217
+ legacy_rel = legacy_relative or omg_relative
218
+ legacy_path = os.path.join(p["legacy_omc"], legacy_rel)
219
+ if os.path.isdir(legacy_path) and auto_migrate:
220
+ migrate_legacy_to_omg(project_dir)
221
+ if os.path.isdir(preferred):
222
+ return preferred
223
+ if os.path.isdir(legacy_path):
224
+ return legacy_path
225
+ return preferred
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from stop_dispatcher import main
4
+
5
+
6
+ if __name__ == "__main__":
7
+ main()