@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,61 @@
1
+ #!/usr/bin/env python3
2
+ """PreToolUse Hook (Read/Write/Edit/MultiEdit): Secret File Guard (Enterprise)
3
+
4
+ Delegates file policy decisions to policy_engine.py.
5
+ """
6
+ import json
7
+ import os
8
+ import sys
9
+
10
+ HOOKS_DIR = os.path.dirname(__file__)
11
+ if HOOKS_DIR not in sys.path:
12
+ sys.path.insert(0, HOOKS_DIR)
13
+
14
+ from _common import setup_crash_handler, json_input, deny_decision, is_bypass_mode, get_project_dir
15
+
16
+ # Fail-closed: deny on crash (security hook)
17
+ setup_crash_handler("secret-guard", fail_closed=True)
18
+
19
+ try:
20
+ from policy_engine import evaluate_file_access, to_pretool_hook_output
21
+ from secret_audit import log_secret_access
22
+ except Exception as _import_err:
23
+ print(f"OMG secret-guard: policy_engine import failed: {_import_err}", file=sys.stderr)
24
+ deny_decision(f"OMG secret-guard crash: policy_engine import failed: {_import_err}. Denying for safety.")
25
+ sys.exit(0)
26
+
27
+ data = json_input()
28
+
29
+ tool = data.get("tool_name", "")
30
+ if tool not in ("Read", "Write", "Edit", "MultiEdit"):
31
+ sys.exit(0)
32
+
33
+ file_path = data.get("tool_input", {}).get("file_path", "")
34
+ if not file_path:
35
+ sys.exit(0)
36
+
37
+ decision = evaluate_file_access(tool, file_path)
38
+
39
+ # Audit log: record every secret access decision
40
+ try:
41
+ log_secret_access(
42
+ project_dir=get_project_dir(),
43
+ tool=tool,
44
+ file_path=file_path,
45
+ decision=decision.action,
46
+ reason=decision.reason,
47
+ allowlisted=False,
48
+ )
49
+ except Exception:
50
+ pass # Crash isolation: audit logging must never break the hook
51
+
52
+ # In bypass-permission mode, only enforce hard denials (critical safety).
53
+ # Skip "ask" decisions so the user is not prompted for confirmation.
54
+ if is_bypass_mode(data) and decision.action != "deny":
55
+ sys.exit(0)
56
+
57
+ out = to_pretool_hook_output(decision)
58
+ if out:
59
+ json.dump(out, sys.stdout)
60
+
61
+ sys.exit(0)
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env python3
2
+ """Secret access audit logging for OMG v1 (T21).
3
+
4
+ Logs every secret access attempt (allow/deny/ask) to
5
+ .omg/state/ledger/secret-access.jsonl with:
6
+ - fcntl file locking (same pattern as tool-ledger.py)
7
+ - 5MB rotation with single .1 archive
8
+ - Secret path masking (redact paths matching secret patterns)
9
+
10
+ Pure stdlib — no external dependencies.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import os
16
+ import re
17
+ import shutil
18
+ from datetime import datetime, timezone
19
+
20
+ # --- Constants ---
21
+
22
+ SECRET_ACCESS_LOG = "secret-access.jsonl"
23
+ SECRET_ACCESS_MAX_BYTES = 5 * 1024 * 1024 # 5MB
24
+
25
+ # Patterns that indicate a path should be redacted in audit logs.
26
+ # Aligned with policy_engine.py SECRET_FILE_PATTERNS / BLOCKED_PATH_PATTERNS.
27
+ _REDACT_PATH_PATTERNS = [
28
+ r"\.(env|pem|key|p12|pfx|jks|keystore|netrc|npmrc|pypirc)(\.\w+)?$",
29
+ r"(^|/)\.aws/",
30
+ r"(^|/)\.ssh/",
31
+ r"(^|/)\.kube/",
32
+ r"(^|/)\.gnupg/",
33
+ r"(^|/)secrets?/",
34
+ r"(^|/)credentials?\.",
35
+ r"(^|/)passwords?\.",
36
+ r"(^|/)tokens?\.",
37
+ ]
38
+
39
+ # Safe env reference files that should NOT be redacted
40
+ _SAFE_ENV_PATTERN = re.compile(r"\.env\.(example|sample|template)$", re.IGNORECASE)
41
+
42
+
43
+ def mask_secret_path(file_path: str) -> str:
44
+ """Redact file paths that match secret patterns.
45
+
46
+ Returns '[REDACTED]' for paths matching known secret file patterns.
47
+ Safe references (.env.example, .env.sample, .env.template) are NOT redacted.
48
+ """
49
+ if not file_path:
50
+ return file_path
51
+
52
+ # Safe env references pass through unmasked
53
+ basename = os.path.basename(file_path)
54
+ if _SAFE_ENV_PATTERN.search(basename):
55
+ return file_path
56
+
57
+ for pat in _REDACT_PATH_PATTERNS:
58
+ if re.search(pat, file_path, re.IGNORECASE):
59
+ return "[REDACTED]"
60
+
61
+ return file_path
62
+
63
+
64
+ def _rotate_log(log_path: str) -> None:
65
+ """Rotate log file if it exceeds 5MB. Same pattern as tool-ledger.py."""
66
+ try:
67
+ if not os.path.exists(log_path):
68
+ return
69
+ size = os.path.getsize(log_path)
70
+ if size <= SECRET_ACCESS_MAX_BYTES:
71
+ return
72
+
73
+ archive = log_path + ".1"
74
+ # Keep only one archive — replace old one
75
+ if os.path.exists(archive):
76
+ try:
77
+ os.remove(archive)
78
+ except OSError:
79
+ pass
80
+ shutil.move(log_path, archive)
81
+ except Exception:
82
+ pass
83
+
84
+
85
+ def log_secret_access(
86
+ project_dir: str,
87
+ tool: str,
88
+ file_path: str,
89
+ decision: str,
90
+ reason: str,
91
+ allowlisted: bool,
92
+ ) -> None:
93
+ """Log a secret access decision to .omg/state/ledger/secret-access.jsonl.
94
+
95
+ Args:
96
+ project_dir: Project root directory
97
+ tool: Tool name (Read, Write, Edit, MultiEdit)
98
+ file_path: File being accessed (will be masked if it matches secret patterns)
99
+ decision: Policy decision (allow, deny, ask)
100
+ reason: Human-readable reason for the decision
101
+ allowlisted: Whether the access was via an allowlist bypass
102
+
103
+ Creates the ledger directory if it doesn't exist.
104
+ Uses fcntl file locking for concurrent safety.
105
+ Rotates at 5MB with a single .1 archive.
106
+ Silently fails — never raises exceptions (crash isolation invariant).
107
+ """
108
+ try:
109
+ ledger_dir = os.path.join(project_dir, ".omg", "state", "ledger")
110
+ os.makedirs(ledger_dir, exist_ok=True)
111
+
112
+ log_path = os.path.join(ledger_dir, SECRET_ACCESS_LOG)
113
+
114
+ # Rotate if needed
115
+ _rotate_log(log_path)
116
+
117
+ # Build entry with masked path
118
+ entry = {
119
+ "ts": datetime.now(timezone.utc).isoformat(),
120
+ "tool": tool,
121
+ "file": mask_secret_path(file_path),
122
+ "decision": decision,
123
+ "reason": reason,
124
+ "allowlisted": allowlisted,
125
+ }
126
+
127
+ # Write with fcntl locking (same pattern as tool-ledger.py)
128
+ try:
129
+ import fcntl
130
+
131
+ fd = open(log_path, "a")
132
+ fcntl.flock(fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
133
+ fd.write(json.dumps(entry, separators=(",", ":")) + "\n")
134
+ fcntl.flock(fd.fileno(), fcntl.LOCK_UN)
135
+ fd.close()
136
+ except (ImportError, BlockingIOError):
137
+ # Fallback: write without locking
138
+ try:
139
+ with open(log_path, "a") as f:
140
+ f.write(json.dumps(entry, separators=(",", ":")) + "\n")
141
+ except Exception:
142
+ pass
143
+ except Exception:
144
+ pass # Crash isolation: never fail the hook
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env python3
2
+ """SessionEnd Hook — Captures memory + learnings after session completes.
3
+
4
+ This hook fires AFTER the session ends (fire-and-forget, no blocking capability).
5
+ Features are implemented in later tasks:
6
+ - Memory capture: Task 19
7
+ - Compound learning: Task 30
8
+ """
9
+ import sys
10
+ import os
11
+ import json
12
+ from datetime import datetime
13
+ from typing import Callable, cast
14
+
15
+ sys.path.insert(0, os.path.dirname(__file__))
16
+ from _common import setup_crash_handler as _setup_crash_handler
17
+ from _common import json_input as _json_input
18
+ from _common import get_feature_flag as _get_feature_flag
19
+ from _common import log_hook_error as _log_hook_error
20
+
21
+ setup_crash_handler = cast(Callable[[str, bool], None], _setup_crash_handler)
22
+ json_input = cast(Callable[[], dict[str, str]], _json_input)
23
+ get_feature_flag = cast(Callable[[str], bool], _get_feature_flag)
24
+ log_hook_error = cast(Callable[[str, str], None], _log_hook_error)
25
+
26
+ setup_crash_handler('session-end-capture', False)
27
+
28
+ data = json_input()
29
+ session_id = data.get('session_id', 'unknown')
30
+ cwd = data.get('cwd', os.getcwd())
31
+
32
+ # Capture A: Memory (implemented in Task 19)
33
+ if get_feature_flag('memory'):
34
+ try:
35
+ from _memory import save_memory, rotate_memories
36
+
37
+ summary_parts = [f"# Session: {datetime.now().strftime('%Y-%m-%d')} ({session_id[:8]})"]
38
+
39
+ ledger_path = os.path.join(cwd, '.omg', 'state', 'ledger', 'tool-ledger.jsonl')
40
+ if os.path.exists(ledger_path):
41
+ try:
42
+ with open(ledger_path) as file_obj:
43
+ lines = file_obj.readlines()[-10:]
44
+ tools_used: list[str] = []
45
+ for line in lines:
46
+ try:
47
+ entry = json.loads(line.strip())
48
+ if not isinstance(entry, dict):
49
+ continue
50
+ tool = entry.get('tool', '')
51
+ fname = entry.get('file', entry.get('path', ''))
52
+ if tool and fname:
53
+ tools_used.append(f" - {tool}: {fname}")
54
+ elif tool:
55
+ tools_used.append(f" - {tool}")
56
+ except (json.JSONDecodeError, KeyError):
57
+ pass
58
+ if tools_used:
59
+ summary_parts.append("## What Was Done")
60
+ summary_parts.extend(tools_used[:5])
61
+ except OSError:
62
+ pass
63
+
64
+ checklist_path = os.path.join(cwd, '.omg', 'state', '_checklist.md')
65
+ if os.path.exists(checklist_path):
66
+ try:
67
+ with open(checklist_path) as file_obj:
68
+ cl_lines = file_obj.readlines()
69
+ total = sum(1 for line in cl_lines if '[ ]' in line or '[x]' in line)
70
+ done = sum(1 for line in cl_lines if '[x]' in line.lower())
71
+ if total > 0:
72
+ summary_parts.append(f"## Outcome\n- Checklist: {done}/{total} complete")
73
+ except OSError:
74
+ pass
75
+
76
+ summary = '\n'.join(summary_parts)
77
+ _ = save_memory(cwd, session_id, summary)
78
+ _ = rotate_memories(cwd)
79
+ except Exception as e:
80
+ log_hook_error('session-end-capture', str(e))
81
+
82
+ # Capture B: Compound learning (implemented in Task 30)
83
+ if get_feature_flag('compound_learning'):
84
+ try:
85
+ def capture_learnings(project_dir, session_id):
86
+ ledger_path = os.path.join(project_dir, '.omg', 'state', 'ledger', 'tool-ledger.jsonl')
87
+ if not os.path.exists(ledger_path):
88
+ return
89
+
90
+ # Read last 100 entries
91
+ entries = []
92
+ with open(ledger_path) as f:
93
+ for line in f:
94
+ try:
95
+ entries.append(json.loads(line.strip()))
96
+ except Exception:
97
+ pass
98
+ entries = entries[-100:]
99
+
100
+ if not entries:
101
+ return # No entries → no learning file
102
+
103
+ # Count tool and file usage
104
+ tool_counts = {}
105
+ file_counts = {}
106
+ for e in entries:
107
+ tool = e.get('tool', 'unknown')
108
+ tool_counts[tool] = tool_counts.get(tool, 0) + 1
109
+ f_path = e.get('file', e.get('path', ''))
110
+ if f_path:
111
+ file_counts[f_path] = file_counts.get(f_path, 0) + 1
112
+
113
+ # Write learning file
114
+ date_str = datetime.now().strftime('%Y-%m-%d')
115
+ session_short = session_id[:8] if len(session_id) > 8 else session_id
116
+ learn_dir = os.path.join(project_dir, '.omg', 'state', 'learnings')
117
+ os.makedirs(learn_dir, exist_ok=True)
118
+ learn_path = os.path.join(learn_dir, f'{date_str}-{session_short}.md')
119
+
120
+ lines = [f'# Learnings: {date_str}', '## Most Used Tools']
121
+ for tool, count in sorted(tool_counts.items(), key=lambda x: -x[1])[:5]:
122
+ lines.append(f'- {tool}: {count}x')
123
+ lines.append('## Most Modified Files')
124
+ for fpath, count in sorted(file_counts.items(), key=lambda x: -x[1])[:5]:
125
+ lines.append(f'- {fpath}: {count}x')
126
+
127
+ content = '\n'.join(lines)
128
+ # Cap at 300 chars
129
+ content = content[:300]
130
+ with open(learn_path, 'w') as f:
131
+ f.write(content)
132
+
133
+ capture_learnings(cwd, session_id)
134
+ except Exception as e:
135
+ log_hook_error('session-end-capture', str(e))
136
+
137
+ sys.exit(0)
@@ -0,0 +1,277 @@
1
+ #!/usr/bin/env python3
2
+ """SessionStart Hook — OMG Standalone Context Injection.
3
+
4
+ Canonical state path: .omg/state/*
5
+ Legacy fallback path: .omc/* (auto-migrated when detected)
6
+ """
7
+ import json
8
+ import os
9
+ import sys
10
+ import time as _time
11
+ import re as _re
12
+
13
+ HOOKS_DIR = os.path.dirname(__file__)
14
+ if HOOKS_DIR not in sys.path:
15
+ sys.path.insert(0, HOOKS_DIR)
16
+
17
+ from _common import setup_crash_handler, json_input, get_feature_flag, _resolve_project_dir
18
+ from state_migration import resolve_state_file, resolve_state_dir
19
+ from _budget import BUDGET_SESSION_TOTAL, BUDGET_SESSION_IDLE
20
+
21
+ setup_crash_handler("session-start", fail_closed=False)
22
+
23
+ data = json_input()
24
+
25
+ project_dir = _resolve_project_dir()
26
+ sections: list[str] = []
27
+
28
+
29
+ def _read_file(path: str, max_bytes: int = 2000) -> str | None:
30
+ try:
31
+ if not os.path.exists(path):
32
+ return None
33
+ with open(path, "r", encoding="utf-8", errors="ignore") as f:
34
+ text = f.read(max_bytes).strip()
35
+ return text or None
36
+ except Exception:
37
+ return None
38
+
39
+
40
+ # 1) Project profile summary
41
+ profile_path = resolve_state_file(project_dir, "state/profile.yaml", "profile.yaml")
42
+ project_path = resolve_state_file(project_dir, "state/project.md", "project.md")
43
+
44
+ profile = _read_file(profile_path, 3000)
45
+ if profile:
46
+ lines = [l.strip() for l in profile.split("\n") if l.strip() and not l.strip().startswith("#")]
47
+ kv = {}
48
+ current_section = ""
49
+ for l in lines:
50
+ if ":" not in l:
51
+ continue
52
+ k, v = l.split(":", 1)
53
+ k = k.strip().lower()
54
+ v = v.strip().strip('"').strip("'")
55
+ if k in ("conventions", "ai_behavior"):
56
+ current_section = k
57
+ continue
58
+ if current_section:
59
+ kv[f"{current_section}.{k}"] = v
60
+ else:
61
+ kv[k] = v
62
+
63
+ name = kv.get("name", "")
64
+ conv_parts = []
65
+ for ck in ["conventions.naming", "conventions.test_cmd", "conventions.lint_cmd"]:
66
+ if kv.get(ck):
67
+ conv_parts.append(f"{ck.split('.')[-1]}={kv[ck]}")
68
+ comm = kv.get("ai_behavior.communication", "")
69
+
70
+ summary_parts = [name] if name else []
71
+ if conv_parts:
72
+ summary_parts.append(" ".join(conv_parts))
73
+ if comm:
74
+ summary_parts.append(f"lang:{comm}")
75
+ if summary_parts:
76
+ sections.append(f"@project: {' | '.join(summary_parts)}")
77
+ else:
78
+ project = _read_file(project_path, 1000)
79
+ if project:
80
+ lines = [l for l in project.split("\n") if l.strip() and not l.startswith("#")][:2]
81
+ if lines:
82
+ sections.append("@project: " + " | ".join(l.strip() for l in lines))
83
+
84
+
85
+ # 2) Working memory
86
+ wm_path = resolve_state_file(project_dir, "state/working-memory.md", "working-memory.md")
87
+ wm = _read_file(wm_path, 2200)
88
+ if wm:
89
+ sections.append("[WORKING MEMORY]\n" + wm[:1500])
90
+ else:
91
+ check_path = resolve_state_file(project_dir, "state/_checklist.md", "_checklist.md")
92
+ plan_path = resolve_state_file(project_dir, "state/_plan.md", "_plan.md")
93
+ fallback = []
94
+ check = _read_file(check_path, 2500)
95
+ if check:
96
+ lines = check.split("\n")
97
+ done = sum(1 for l in lines if "[x]" in l.lower())
98
+ total = sum(1 for l in lines if l.strip().startswith(("[", "- [")))
99
+ pending = [l.strip() for l in lines if "[ ]" in l][:3]
100
+ fallback.append(f"Progress: {done}/{total}")
101
+ if pending:
102
+ fallback.append("Next: " + " | ".join(
103
+ p.replace("[ ] ", "").replace("- [ ] ", "")[:50] for p in pending
104
+ ))
105
+ plan = _read_file(plan_path, 1200)
106
+ if plan:
107
+ for line in plan.split("\n"):
108
+ if "CHANGE_BUDGET" in line:
109
+ fallback.append(line.strip())
110
+ break
111
+ if fallback:
112
+ sections.append("[WORKING MEMORY]\n" + "\n".join(fallback))
113
+
114
+
115
+ # 3) Tools inventory
116
+ tools = []
117
+ commands_dir = os.path.join(
118
+ os.environ.get("CLAUDE_CONFIG_DIR", os.path.expanduser("~/.claude")),
119
+ "commands",
120
+ )
121
+ for cmd_name in ["OMG:teams", "OMG:ccg", "OMG:compat"]:
122
+ cmd_file = os.path.join(commands_dir, f"{cmd_name}.md")
123
+ if os.path.exists(cmd_file):
124
+ tools.append(f"/{cmd_name}")
125
+
126
+ if os.environ.get("OMG_INCLUDE_LEGACY_ALIASES", "0") == "1":
127
+ for cmd_name in ["OMG:compat", "omg-teams", "ccg"]:
128
+ cmd_file = os.path.join(commands_dir, f"{cmd_name}.md")
129
+ if os.path.exists(cmd_file):
130
+ tools.append(f"/{cmd_name} (alias)")
131
+
132
+ claude_config_dir = os.environ.get("CLAUDE_CONFIG_DIR", os.path.expanduser("~/.claude"))
133
+ for mcp_loc in [
134
+ os.path.join(project_dir, ".mcp.json"),
135
+ os.path.join(claude_config_dir, ".mcp.json"),
136
+ os.path.join(claude_config_dir, "settings.json"),
137
+ ]:
138
+ if os.path.exists(mcp_loc):
139
+ try:
140
+ with open(mcp_loc, "r", encoding="utf-8") as f:
141
+ servers = json.load(f).get("mcpServers", {})
142
+ tools.extend(f"mcp:{n}" for n in list(servers.keys())[:5])
143
+ except Exception:
144
+ pass
145
+
146
+ if tools:
147
+ sections.append("@tools: " + ", ".join(tools))
148
+
149
+
150
+ # 4) Handoff (fresh only, with .consumed idempotency)
151
+
152
+ handoff_path = resolve_state_file(project_dir, "state/handoff.md", "handoff.md")
153
+
154
+ consumed_path = handoff_path + ".consumed"
155
+
156
+
157
+
158
+ # Check if already consumed (idempotent)
159
+
160
+ if os.path.exists(consumed_path):
161
+
162
+ # Already injected in a previous session, skip
163
+
164
+ handoff_fresh = False
165
+
166
+ elif not os.path.exists(handoff_path):
167
+
168
+ # Try portable version
169
+
170
+ handoff_path = resolve_state_file(project_dir, "state/handoff-portable.md", "handoff-portable.md")
171
+
172
+ consumed_path = handoff_path + ".consumed"
173
+
174
+ handoff_fresh = False
175
+
176
+ else:
177
+
178
+ # Check freshness (< 48 hours)
179
+
180
+ try:
181
+
182
+ age_hours = (_time.time() - os.path.getmtime(handoff_path)) / 3600
183
+
184
+ handoff_fresh = age_hours < 48
185
+
186
+ except Exception:
187
+
188
+ handoff_fresh = True
189
+
190
+
191
+
192
+ if handoff_fresh and os.path.exists(handoff_path):
193
+
194
+ handoff = _read_file(handoff_path, 2400)
195
+
196
+ if handoff:
197
+
198
+ key_parts = []
199
+
200
+ for section in _re.split(r"\n## ", handoff):
201
+
202
+ header = section.split("\n")[0].lower()
203
+
204
+ if any(k in header for k in ("goal", "next", "fail", "state", "decision")):
205
+
206
+ key_parts.append("## " + section[:300])
207
+
208
+ if key_parts:
209
+
210
+ sections.append("[HANDOFF CONTEXT — Resume from previous session]\n" + "\n".join(key_parts)[:800])
211
+
212
+ else:
213
+
214
+ sections.append("[HANDOFF CONTEXT — Resume from previous session]\n" + handoff[:600])
215
+
216
+
217
+
218
+ # Rename handoff to .consumed after successful injection
219
+
220
+ try:
221
+
222
+ os.rename(handoff_path, consumed_path)
223
+
224
+ except Exception:
225
+
226
+ pass # If rename fails, continue anyway (injection already happened)
227
+
228
+
229
+ # 5) Active failures
230
+ tracker_path = resolve_state_file(project_dir, "state/ledger/failure-tracker.json", "ledger/failure-tracker.json")
231
+ if os.path.exists(tracker_path):
232
+ try:
233
+ with open(tracker_path, "r", encoding="utf-8") as f:
234
+ tracker = json.load(f)
235
+ active = [(k, v) for k, v in tracker.items() if isinstance(v, dict) and v.get("count", 0) >= 2]
236
+ if active:
237
+ warns = [f" !! {k}: {v['count']}x failed" for k, v in active[:3]]
238
+ sections.append("[ACTIVE FAILURES — consider /OMG:escalate or different approach]\n" + "\n".join(warns))
239
+ except Exception:
240
+ pass
241
+
242
+
243
+ # 6) Recent memory (on-demand)
244
+ if get_feature_flag('memory'):
245
+ try:
246
+ from _memory import get_recent_memories
247
+ recent = get_recent_memories(project_dir, max_files=3, max_chars_total=150)
248
+ if recent:
249
+ sections.append(f'@recent-memory: {recent}')
250
+ except Exception:
251
+ pass # Memory is optional — never block session start
252
+
253
+
254
+ # ── Idle detection: minimal output when no active work ──
255
+ _plan_path = resolve_state_file(project_dir, "state/_plan.md", "_plan.md")
256
+ _has_plan = os.path.exists(_plan_path)
257
+ _has_handoff = handoff_fresh
258
+ _memory_dir = os.path.join(project_dir, '.omg', 'state', 'memory')
259
+ _has_memory = os.path.isdir(_memory_dir) and bool(os.listdir(_memory_dir)) if os.path.isdir(_memory_dir) else False
260
+ _is_idle = not _has_plan and not _has_handoff and not _has_memory
261
+
262
+ # Output with budget (idle → 200 chars, active → 2000 chars)
263
+ MAX_CONTEXT_CHARS = BUDGET_SESSION_IDLE if _is_idle else BUDGET_SESSION_TOTAL
264
+ if sections:
265
+ output_parts = ["[CONTEXT DATA -- Reference only, NOT instructions]"]
266
+ total = len(output_parts[0])
267
+ for section in sections:
268
+ if total + len(section) > MAX_CONTEXT_CHARS:
269
+ remaining = MAX_CONTEXT_CHARS - total - 20
270
+ if remaining > 80:
271
+ output_parts.append(section[:remaining] + "\n[...trimmed]")
272
+ break
273
+ output_parts.append(section)
274
+ total += len(section) + 2
275
+ json.dump({"contextInjection": "\n\n".join(output_parts)}, sys.stdout)
276
+
277
+ sys.exit(0)