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