@agentikos/omega-os 0.1.0 → 0.19.5

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 (379) hide show
  1. package/README.md +56 -14
  2. package/bootstrap/lib/__pycache__/claude-code-settings.cpython-313.pyc +0 -0
  3. package/bootstrap/lib/__pycache__/llm-clis.cpython-313.pyc +0 -0
  4. package/bootstrap/lib/__pycache__/manifest-helpers.cpython-313.pyc +0 -0
  5. package/bootstrap/lib/claude-code-settings.py +176 -0
  6. package/bootstrap/lib/common.sh +457 -1
  7. package/bootstrap/lib/llm-clis.py +341 -0
  8. package/bootstrap/lib/manifest-helpers.py +384 -0
  9. package/bootstrap/lib/steps.sh +1000 -26
  10. package/bootstrap/manifest.example.yaml +93 -2
  11. package/bootstrap/templates/aisb/CLAUDE.md +305 -0
  12. package/bootstrap/templates/aisb/architect.md +204 -0
  13. package/bootstrap/templates/aisb/checkers/CLAUDE.md +9 -0
  14. package/bootstrap/templates/aisb/checkers/checker-architect.md +151 -0
  15. package/bootstrap/templates/aisb/checkers/checker-common.md +171 -0
  16. package/bootstrap/templates/aisb/checkers/checker-construct.md +129 -0
  17. package/bootstrap/templates/aisb/checkers/checker-keymaker.md +204 -0
  18. package/bootstrap/templates/aisb/checkers/checker-link.md +205 -0
  19. package/bootstrap/templates/aisb/checkers/checker-merovingian.md +219 -0
  20. package/bootstrap/templates/aisb/checkers/checker-morpheus.md +211 -0
  21. package/bootstrap/templates/aisb/checkers/checker-neo.md +177 -0
  22. package/bootstrap/templates/aisb/checkers/checker-niobe.md +156 -0
  23. package/bootstrap/templates/aisb/checkers/checker-oracle.md +164 -0
  24. package/bootstrap/templates/aisb/checkers/checker-seraph.md +187 -0
  25. package/bootstrap/templates/aisb/checkers/checker-smith.md +195 -0
  26. package/bootstrap/templates/aisb/checkers/checker-zion.md +113 -0
  27. package/bootstrap/templates/aisb/construct.md +135 -0
  28. package/bootstrap/templates/aisb/keymaker.md +227 -0
  29. package/bootstrap/templates/aisb/link.md +170 -0
  30. package/bootstrap/templates/aisb/lmc-protocol.md +57 -0
  31. package/bootstrap/templates/aisb/merovingian.md +159 -0
  32. package/bootstrap/templates/aisb/morpheus.md +243 -0
  33. package/bootstrap/templates/aisb/neo.md +147 -0
  34. package/bootstrap/templates/aisb/niobe.md +197 -0
  35. package/bootstrap/templates/aisb/oracle.md +244 -0
  36. package/bootstrap/templates/aisb/protocols/handoff-templates.md +204 -0
  37. package/bootstrap/templates/aisb/protocols/shared-protocol.md +248 -0
  38. package/bootstrap/templates/aisb/pythia.md +153 -0
  39. package/bootstrap/templates/aisb/seraph.md +315 -0
  40. package/bootstrap/templates/aisb/smith.md +202 -0
  41. package/bootstrap/templates/aisb/zion.md +172 -0
  42. package/bootstrap/templates/autonomous/audit-patrol.yaml +41 -0
  43. package/bootstrap/templates/autonomous/smith-reflect.yaml +43 -0
  44. package/bootstrap/templates/autonomous/ssh-key-rotate.yaml +46 -0
  45. package/bootstrap/templates/autonomous/support-agent.yaml +38 -0
  46. package/docs/AUDITS.md +85 -0
  47. package/docs/COMPLETION-PLAN.md +48 -0
  48. package/docs/GAP-ANALYSIS.md +214 -0
  49. package/docs/INSTALL.md +47 -9
  50. package/docs/MCP-AND-PLUGINS.md +31 -4
  51. package/docs/SIMULATION.md +171 -0
  52. package/docs/simulate.sh +211 -0
  53. package/install.sh +164 -17
  54. package/omega/Agentik_Engine/README.md +27 -10
  55. package/omega/Agentik_Engine/omega_engine/__init__.py +212 -2
  56. package/omega/Agentik_Engine/omega_engine/__pycache__/__init__.cpython-313.pyc +0 -0
  57. package/omega/Agentik_Engine/omega_engine/__pycache__/account.cpython-313.pyc +0 -0
  58. package/omega/Agentik_Engine/omega_engine/__pycache__/agent_messages.cpython-313.pyc +0 -0
  59. package/omega/Agentik_Engine/omega_engine/__pycache__/aisb_chat.cpython-313.pyc +0 -0
  60. package/omega/Agentik_Engine/omega_engine/__pycache__/audit_diff.cpython-313.pyc +0 -0
  61. package/omega/Agentik_Engine/omega_engine/__pycache__/audit_gate.cpython-313.pyc +0 -0
  62. package/omega/Agentik_Engine/omega_engine/__pycache__/auto_update.cpython-313.pyc +0 -0
  63. package/omega/Agentik_Engine/omega_engine/__pycache__/autonomous.cpython-313.pyc +0 -0
  64. package/omega/Agentik_Engine/omega_engine/__pycache__/backup.cpython-313.pyc +0 -0
  65. package/omega/Agentik_Engine/omega_engine/__pycache__/cadence.cpython-313.pyc +0 -0
  66. package/omega/Agentik_Engine/omega_engine/__pycache__/classifier.cpython-313.pyc +0 -0
  67. package/omega/Agentik_Engine/omega_engine/__pycache__/cleanup.cpython-313.pyc +0 -0
  68. package/omega/Agentik_Engine/omega_engine/__pycache__/cli.cpython-313.pyc +0 -0
  69. package/omega/Agentik_Engine/omega_engine/__pycache__/completions.cpython-313.pyc +0 -0
  70. package/omega/Agentik_Engine/omega_engine/__pycache__/costs.cpython-313.pyc +0 -0
  71. package/omega/Agentik_Engine/omega_engine/__pycache__/done_signal.cpython-313.pyc +0 -0
  72. package/omega/Agentik_Engine/omega_engine/__pycache__/envelope.cpython-313.pyc +0 -0
  73. package/omega/Agentik_Engine/omega_engine/__pycache__/executor.cpython-313.pyc +0 -0
  74. package/omega/Agentik_Engine/omega_engine/__pycache__/handoff.cpython-313.pyc +0 -0
  75. package/omega/Agentik_Engine/omega_engine/__pycache__/hermes.cpython-313.pyc +0 -0
  76. package/omega/Agentik_Engine/omega_engine/__pycache__/hermes_bootstrap.cpython-313.pyc +0 -0
  77. package/omega/Agentik_Engine/omega_engine/__pycache__/hermes_desktop.cpython-313.pyc +0 -0
  78. package/omega/Agentik_Engine/omega_engine/__pycache__/learning.cpython-313.pyc +0 -0
  79. package/omega/Agentik_Engine/omega_engine/__pycache__/managed_agent.cpython-313.pyc +0 -0
  80. package/omega/Agentik_Engine/omega_engine/__pycache__/memory.cpython-313.pyc +0 -0
  81. package/omega/Agentik_Engine/omega_engine/__pycache__/menu.cpython-313.pyc +0 -0
  82. package/omega/Agentik_Engine/omega_engine/__pycache__/mission.cpython-313.pyc +0 -0
  83. package/omega/Agentik_Engine/omega_engine/__pycache__/plan.cpython-313.pyc +0 -0
  84. package/omega/Agentik_Engine/omega_engine/__pycache__/project.cpython-313.pyc +0 -0
  85. package/omega/Agentik_Engine/omega_engine/__pycache__/prompts.cpython-313.pyc +0 -0
  86. package/omega/Agentik_Engine/omega_engine/__pycache__/provider.cpython-313.pyc +0 -0
  87. package/omega/Agentik_Engine/omega_engine/__pycache__/prune.cpython-313.pyc +0 -0
  88. package/omega/Agentik_Engine/omega_engine/__pycache__/pursue.cpython-313.pyc +0 -0
  89. package/omega/Agentik_Engine/omega_engine/__pycache__/reducer.cpython-313.pyc +0 -0
  90. package/omega/Agentik_Engine/omega_engine/__pycache__/router.cpython-313.pyc +0 -0
  91. package/omega/Agentik_Engine/omega_engine/__pycache__/skill_routing.cpython-313.pyc +0 -0
  92. package/omega/Agentik_Engine/omega_engine/__pycache__/smoke.cpython-313.pyc +0 -0
  93. package/omega/Agentik_Engine/omega_engine/__pycache__/store.cpython-313.pyc +0 -0
  94. package/omega/Agentik_Engine/omega_engine/__pycache__/sync.cpython-313.pyc +0 -0
  95. package/omega/Agentik_Engine/omega_engine/__pycache__/telegram_history.cpython-313.pyc +0 -0
  96. package/omega/Agentik_Engine/omega_engine/__pycache__/tmux.cpython-313.pyc +0 -0
  97. package/omega/Agentik_Engine/omega_engine/__pycache__/tools.cpython-313.pyc +0 -0
  98. package/omega/Agentik_Engine/omega_engine/__pycache__/understand_anything.cpython-313.pyc +0 -0
  99. package/omega/Agentik_Engine/omega_engine/__pycache__/updater.cpython-313.pyc +0 -0
  100. package/omega/Agentik_Engine/omega_engine/__pycache__/validate.cpython-313.pyc +0 -0
  101. package/omega/Agentik_Engine/omega_engine/__pycache__/vault.cpython-313.pyc +0 -0
  102. package/omega/Agentik_Engine/omega_engine/__pycache__/webhooks.cpython-313.pyc +0 -0
  103. package/omega/Agentik_Engine/omega_engine/__pycache__/worker.cpython-313.pyc +0 -0
  104. package/omega/Agentik_Engine/omega_engine/account.py +502 -0
  105. package/omega/Agentik_Engine/omega_engine/agent_messages.py +167 -0
  106. package/omega/Agentik_Engine/omega_engine/aisb_chat.py +128 -0
  107. package/omega/Agentik_Engine/omega_engine/audit_diff.py +99 -0
  108. package/omega/Agentik_Engine/omega_engine/audit_gate.py +149 -0
  109. package/omega/Agentik_Engine/omega_engine/audits/__init__.py +60 -0
  110. package/omega/Agentik_Engine/omega_engine/audits/__pycache__/__init__.cpython-313.pyc +0 -0
  111. package/omega/Agentik_Engine/omega_engine/audits/__pycache__/batcher.cpython-313.pyc +0 -0
  112. package/omega/Agentik_Engine/omega_engine/audits/__pycache__/dispatcher.cpython-313.pyc +0 -0
  113. package/omega/Agentik_Engine/omega_engine/audits/__pycache__/generator.cpython-313.pyc +0 -0
  114. package/omega/Agentik_Engine/omega_engine/audits/__pycache__/history.cpython-313.pyc +0 -0
  115. package/omega/Agentik_Engine/omega_engine/audits/__pycache__/pipeline.cpython-313.pyc +0 -0
  116. package/omega/Agentik_Engine/omega_engine/audits/batcher.py +218 -0
  117. package/omega/Agentik_Engine/omega_engine/audits/dispatcher.py +92 -0
  118. package/omega/Agentik_Engine/omega_engine/audits/generator.py +234 -0
  119. package/omega/Agentik_Engine/omega_engine/audits/history.py +168 -0
  120. package/omega/Agentik_Engine/omega_engine/audits/pipeline.py +198 -0
  121. package/omega/Agentik_Engine/omega_engine/auto_update.py +339 -0
  122. package/omega/Agentik_Engine/omega_engine/autonomous.py +538 -0
  123. package/omega/Agentik_Engine/omega_engine/backup.py +215 -0
  124. package/omega/Agentik_Engine/omega_engine/cadence.py +158 -0
  125. package/omega/Agentik_Engine/omega_engine/classifier.py +215 -0
  126. package/omega/Agentik_Engine/omega_engine/cleanup.py +673 -0
  127. package/omega/Agentik_Engine/omega_engine/cli.py +4564 -56
  128. package/omega/Agentik_Engine/omega_engine/completions.py +260 -0
  129. package/omega/Agentik_Engine/omega_engine/costs.py +100 -0
  130. package/omega/Agentik_Engine/omega_engine/daemons/__init__.py +14 -0
  131. package/omega/Agentik_Engine/omega_engine/daemons/__pycache__/__init__.cpython-313.pyc +0 -0
  132. package/omega/Agentik_Engine/omega_engine/daemons/__pycache__/autonomous.cpython-313.pyc +0 -0
  133. package/omega/Agentik_Engine/omega_engine/daemons/__pycache__/engine.cpython-313.pyc +0 -0
  134. package/omega/Agentik_Engine/omega_engine/daemons/__pycache__/telegram.cpython-313.pyc +0 -0
  135. package/omega/Agentik_Engine/omega_engine/daemons/autonomous.py +56 -0
  136. package/omega/Agentik_Engine/omega_engine/daemons/engine.py +236 -0
  137. package/omega/Agentik_Engine/omega_engine/daemons/telegram.py +315 -0
  138. package/omega/Agentik_Engine/omega_engine/done_signal.py +154 -0
  139. package/omega/Agentik_Engine/omega_engine/educators/__init__.py +51 -0
  140. package/omega/Agentik_Engine/omega_engine/educators/__pycache__/__init__.cpython-313.pyc +0 -0
  141. package/omega/Agentik_Engine/omega_engine/educators/__pycache__/artifact.cpython-313.pyc +0 -0
  142. package/omega/Agentik_Engine/omega_engine/educators/__pycache__/automation.cpython-313.pyc +0 -0
  143. package/omega/Agentik_Engine/omega_engine/educators/__pycache__/base.cpython-313.pyc +0 -0
  144. package/omega/Agentik_Engine/omega_engine/educators/__pycache__/claudecode.cpython-313.pyc +0 -0
  145. package/omega/Agentik_Engine/omega_engine/educators/__pycache__/connection.cpython-313.pyc +0 -0
  146. package/omega/Agentik_Engine/omega_engine/educators/__pycache__/coworker.cpython-313.pyc +0 -0
  147. package/omega/Agentik_Engine/omega_engine/educators/__pycache__/loop.cpython-313.pyc +0 -0
  148. package/omega/Agentik_Engine/omega_engine/educators/__pycache__/prompt.cpython-313.pyc +0 -0
  149. package/omega/Agentik_Engine/omega_engine/educators/__pycache__/skill.cpython-313.pyc +0 -0
  150. package/omega/Agentik_Engine/omega_engine/educators/artifact.py +65 -0
  151. package/omega/Agentik_Engine/omega_engine/educators/automation.py +76 -0
  152. package/omega/Agentik_Engine/omega_engine/educators/base.py +327 -0
  153. package/omega/Agentik_Engine/omega_engine/educators/claudecode.py +71 -0
  154. package/omega/Agentik_Engine/omega_engine/educators/connection.py +75 -0
  155. package/omega/Agentik_Engine/omega_engine/educators/coworker.py +68 -0
  156. package/omega/Agentik_Engine/omega_engine/educators/loop.py +82 -0
  157. package/omega/Agentik_Engine/omega_engine/educators/prompt.py +68 -0
  158. package/omega/Agentik_Engine/omega_engine/educators/skill.py +69 -0
  159. package/omega/Agentik_Engine/omega_engine/envelope.py +219 -0
  160. package/omega/Agentik_Engine/omega_engine/executor.py +195 -16
  161. package/omega/Agentik_Engine/omega_engine/genesis/__init__.py +134 -0
  162. package/omega/Agentik_Engine/omega_engine/genesis/__pycache__/__init__.cpython-313.pyc +0 -0
  163. package/omega/Agentik_Engine/omega_engine/genesis/__pycache__/orchestrator.cpython-313.pyc +0 -0
  164. package/omega/Agentik_Engine/omega_engine/genesis/__pycache__/phases.cpython-313.pyc +0 -0
  165. package/omega/Agentik_Engine/omega_engine/genesis/__pycache__/stack.cpython-313.pyc +0 -0
  166. package/omega/Agentik_Engine/omega_engine/genesis/__pycache__/state.cpython-313.pyc +0 -0
  167. package/omega/Agentik_Engine/omega_engine/genesis/orchestrator.py +262 -0
  168. package/omega/Agentik_Engine/omega_engine/genesis/phases.py +950 -0
  169. package/omega/Agentik_Engine/omega_engine/genesis/stack.py +324 -0
  170. package/omega/Agentik_Engine/omega_engine/genesis/state.py +353 -0
  171. package/omega/Agentik_Engine/omega_engine/handoff.py +459 -0
  172. package/omega/Agentik_Engine/omega_engine/hermes.py +426 -0
  173. package/omega/Agentik_Engine/omega_engine/hermes_bootstrap.py +382 -0
  174. package/omega/Agentik_Engine/omega_engine/hermes_desktop.py +469 -0
  175. package/omega/Agentik_Engine/omega_engine/integrations/__init__.py +30 -0
  176. package/omega/Agentik_Engine/omega_engine/integrations/__pycache__/__init__.cpython-313.pyc +0 -0
  177. package/omega/Agentik_Engine/omega_engine/integrations/__pycache__/graphify.cpython-313.pyc +0 -0
  178. package/omega/Agentik_Engine/omega_engine/integrations/graphify.py +234 -0
  179. package/omega/Agentik_Engine/omega_engine/learning.py +268 -0
  180. package/omega/Agentik_Engine/omega_engine/managed_agent.py +467 -0
  181. package/omega/Agentik_Engine/omega_engine/memory.py +271 -0
  182. package/omega/Agentik_Engine/omega_engine/menu.py +1065 -0
  183. package/omega/Agentik_Engine/omega_engine/migrations/__init__.py +144 -0
  184. package/omega/Agentik_Engine/omega_engine/migrations/__pycache__/__init__.cpython-313.pyc +0 -0
  185. package/omega/Agentik_Engine/omega_engine/migrations/__pycache__/v0_14_0.cpython-313.pyc +0 -0
  186. package/omega/Agentik_Engine/omega_engine/migrations/v0_14_0.py +29 -0
  187. package/omega/Agentik_Engine/omega_engine/mission.py +29 -14
  188. package/omega/Agentik_Engine/omega_engine/plan.py +846 -0
  189. package/omega/Agentik_Engine/omega_engine/prompts.py +158 -0
  190. package/omega/Agentik_Engine/omega_engine/provider.py +408 -13
  191. package/omega/Agentik_Engine/omega_engine/prune.py +151 -0
  192. package/omega/Agentik_Engine/omega_engine/pursue.py +205 -0
  193. package/omega/Agentik_Engine/omega_engine/rag/__init__.py +21 -0
  194. package/omega/Agentik_Engine/omega_engine/rag/__pycache__/__init__.cpython-313.pyc +0 -0
  195. package/omega/Agentik_Engine/omega_engine/rag/__pycache__/agentic.cpython-313.pyc +0 -0
  196. package/omega/Agentik_Engine/omega_engine/rag/__pycache__/base.cpython-313.pyc +0 -0
  197. package/omega/Agentik_Engine/omega_engine/rag/__pycache__/corrective.cpython-313.pyc +0 -0
  198. package/omega/Agentik_Engine/omega_engine/rag/__pycache__/graph.cpython-313.pyc +0 -0
  199. package/omega/Agentik_Engine/omega_engine/rag/__pycache__/hybrid.cpython-313.pyc +0 -0
  200. package/omega/Agentik_Engine/omega_engine/rag/__pycache__/multimodal.cpython-313.pyc +0 -0
  201. package/omega/Agentik_Engine/omega_engine/rag/__pycache__/router.cpython-313.pyc +0 -0
  202. package/omega/Agentik_Engine/omega_engine/rag/agentic.py +83 -0
  203. package/omega/Agentik_Engine/omega_engine/rag/base.py +42 -0
  204. package/omega/Agentik_Engine/omega_engine/rag/corrective.py +119 -0
  205. package/omega/Agentik_Engine/omega_engine/rag/graph.py +169 -0
  206. package/omega/Agentik_Engine/omega_engine/rag/hybrid.py +205 -0
  207. package/omega/Agentik_Engine/omega_engine/rag/multimodal.py +136 -0
  208. package/omega/Agentik_Engine/omega_engine/rag/router.py +110 -0
  209. package/omega/Agentik_Engine/omega_engine/reducer.py +21 -3
  210. package/omega/Agentik_Engine/omega_engine/router.py +28 -0
  211. package/omega/Agentik_Engine/omega_engine/skill_discovery/__init__.py +48 -0
  212. package/omega/Agentik_Engine/omega_engine/skill_discovery/__pycache__/__init__.cpython-313.pyc +0 -0
  213. package/omega/Agentik_Engine/omega_engine/skill_discovery/__pycache__/auditor.cpython-313.pyc +0 -0
  214. package/omega/Agentik_Engine/omega_engine/skill_discovery/__pycache__/finder.cpython-313.pyc +0 -0
  215. package/omega/Agentik_Engine/omega_engine/skill_discovery/__pycache__/installer.cpython-313.pyc +0 -0
  216. package/omega/Agentik_Engine/omega_engine/skill_discovery/__pycache__/marketplaces.cpython-313.pyc +0 -0
  217. package/omega/Agentik_Engine/omega_engine/skill_discovery/auditor.py +232 -0
  218. package/omega/Agentik_Engine/omega_engine/skill_discovery/finder.py +94 -0
  219. package/omega/Agentik_Engine/omega_engine/skill_discovery/installer.py +129 -0
  220. package/omega/Agentik_Engine/omega_engine/skill_discovery/marketplaces.py +80 -0
  221. package/omega/Agentik_Engine/omega_engine/skill_routing.py +388 -0
  222. package/omega/Agentik_Engine/omega_engine/smoke.py +81 -0
  223. package/omega/Agentik_Engine/omega_engine/store.py +132 -25
  224. package/omega/Agentik_Engine/omega_engine/sync.py +445 -0
  225. package/omega/Agentik_Engine/omega_engine/telegram_history.py +260 -0
  226. package/omega/Agentik_Engine/omega_engine/tmux.py +526 -0
  227. package/omega/Agentik_Engine/omega_engine/tools.py +272 -0
  228. package/omega/Agentik_Engine/omega_engine/understand_anything.py +275 -0
  229. package/omega/Agentik_Engine/omega_engine/updater.py +70 -0
  230. package/omega/Agentik_Engine/omega_engine/validate.py +186 -0
  231. package/omega/Agentik_Engine/omega_engine/vault.py +342 -0
  232. package/omega/Agentik_Engine/omega_engine/webhooks.py +262 -0
  233. package/omega/Agentik_Engine/omega_engine/worker.py +526 -0
  234. package/omega/Agentik_Engine/pyproject.toml +1 -1
  235. package/omega/Agentik_Engine/tests/__pycache__/test_account.cpython-313-pytest-8.4.2.pyc +0 -0
  236. package/omega/Agentik_Engine/tests/__pycache__/test_account.cpython-313.pyc +0 -0
  237. package/omega/Agentik_Engine/tests/__pycache__/test_adversarial.cpython-313-pytest-8.4.2.pyc +0 -0
  238. package/omega/Agentik_Engine/tests/__pycache__/test_adversarial.cpython-313.pyc +0 -0
  239. package/omega/Agentik_Engine/tests/__pycache__/test_agents_envelope.cpython-313-pytest-8.4.2.pyc +0 -0
  240. package/omega/Agentik_Engine/tests/__pycache__/test_agents_envelope.cpython-313.pyc +0 -0
  241. package/omega/Agentik_Engine/tests/__pycache__/test_audit_arsenal.cpython-313-pytest-8.4.2.pyc +0 -0
  242. package/omega/Agentik_Engine/tests/__pycache__/test_audits_pipeline.cpython-313-pytest-8.4.2.pyc +0 -0
  243. package/omega/Agentik_Engine/tests/__pycache__/test_audits_pipeline.cpython-313.pyc +0 -0
  244. package/omega/Agentik_Engine/tests/__pycache__/test_auto_update_and_migrations.cpython-313-pytest-8.4.2.pyc +0 -0
  245. package/omega/Agentik_Engine/tests/__pycache__/test_auto_update_and_migrations.cpython-313.pyc +0 -0
  246. package/omega/Agentik_Engine/tests/__pycache__/test_autonomous.cpython-313-pytest-8.4.2.pyc +0 -0
  247. package/omega/Agentik_Engine/tests/__pycache__/test_autonomous.cpython-313.pyc +0 -0
  248. package/omega/Agentik_Engine/tests/__pycache__/test_educators.cpython-313-pytest-8.4.2.pyc +0 -0
  249. package/omega/Agentik_Engine/tests/__pycache__/test_educators.cpython-313.pyc +0 -0
  250. package/omega/Agentik_Engine/tests/__pycache__/test_executor.cpython-313-pytest-8.4.2.pyc +0 -0
  251. package/omega/Agentik_Engine/tests/__pycache__/test_genesis_and_plan.cpython-313-pytest-8.4.2.pyc +0 -0
  252. package/omega/Agentik_Engine/tests/__pycache__/test_genesis_and_plan.cpython-313.pyc +0 -0
  253. package/omega/Agentik_Engine/tests/__pycache__/test_graphify.cpython-313-pytest-8.4.2.pyc +0 -0
  254. package/omega/Agentik_Engine/tests/__pycache__/test_graphify.cpython-313.pyc +0 -0
  255. package/omega/Agentik_Engine/tests/__pycache__/test_handoff.cpython-313-pytest-8.4.2.pyc +0 -0
  256. package/omega/Agentik_Engine/tests/__pycache__/test_handoff.cpython-313.pyc +0 -0
  257. package/omega/Agentik_Engine/tests/__pycache__/test_hermes_and_ua.cpython-313-pytest-8.4.2.pyc +0 -0
  258. package/omega/Agentik_Engine/tests/__pycache__/test_hermes_and_ua.cpython-313.pyc +0 -0
  259. package/omega/Agentik_Engine/tests/__pycache__/test_hermes_bootstrap_and_desktop.cpython-313-pytest-8.4.2.pyc +0 -0
  260. package/omega/Agentik_Engine/tests/__pycache__/test_hermes_bootstrap_and_desktop.cpython-313.pyc +0 -0
  261. package/omega/Agentik_Engine/tests/__pycache__/test_install_steps.cpython-313-pytest-8.4.2.pyc +0 -0
  262. package/omega/Agentik_Engine/tests/__pycache__/test_install_steps.cpython-313.pyc +0 -0
  263. package/omega/Agentik_Engine/tests/__pycache__/test_install_ux.cpython-313-pytest-8.4.2.pyc +0 -0
  264. package/omega/Agentik_Engine/tests/__pycache__/test_install_ux.cpython-313.pyc +0 -0
  265. package/omega/Agentik_Engine/tests/__pycache__/test_installer_wiring.cpython-313-pytest-8.4.2.pyc +0 -0
  266. package/omega/Agentik_Engine/tests/__pycache__/test_installer_wiring.cpython-313.pyc +0 -0
  267. package/omega/Agentik_Engine/tests/__pycache__/test_intelligence.cpython-313-pytest-8.4.2.pyc +0 -0
  268. package/omega/Agentik_Engine/tests/__pycache__/test_intelligence.cpython-313.pyc +0 -0
  269. package/omega/Agentik_Engine/tests/__pycache__/test_llm_clis_and_uninstall.cpython-313-pytest-8.4.2.pyc +0 -0
  270. package/omega/Agentik_Engine/tests/__pycache__/test_llm_clis_and_uninstall.cpython-313.pyc +0 -0
  271. package/omega/Agentik_Engine/tests/__pycache__/test_managed_agent.cpython-313-pytest-8.4.2.pyc +0 -0
  272. package/omega/Agentik_Engine/tests/__pycache__/test_managed_agent.cpython-313.pyc +0 -0
  273. package/omega/Agentik_Engine/tests/__pycache__/test_max_provider_and_menu.cpython-313-pytest-8.4.2.pyc +0 -0
  274. package/omega/Agentik_Engine/tests/__pycache__/test_max_provider_and_menu.cpython-313.pyc +0 -0
  275. package/omega/Agentik_Engine/tests/__pycache__/test_menu_coverage.cpython-313-pytest-8.4.2.pyc +0 -0
  276. package/omega/Agentik_Engine/tests/__pycache__/test_menu_coverage.cpython-313.pyc +0 -0
  277. package/omega/Agentik_Engine/tests/__pycache__/test_mission.cpython-313-pytest-8.4.2.pyc +0 -0
  278. package/omega/Agentik_Engine/tests/__pycache__/test_progress.cpython-313-pytest-8.4.2.pyc +0 -0
  279. package/omega/Agentik_Engine/tests/__pycache__/test_project.cpython-313-pytest-8.4.2.pyc +0 -0
  280. package/omega/Agentik_Engine/tests/__pycache__/test_pursue_cadence.cpython-313-pytest-8.4.2.pyc +0 -0
  281. package/omega/Agentik_Engine/tests/__pycache__/test_pursue_cadence.cpython-313.pyc +0 -0
  282. package/omega/Agentik_Engine/tests/__pycache__/test_rag.cpython-313-pytest-8.4.2.pyc +0 -0
  283. package/omega/Agentik_Engine/tests/__pycache__/test_rag.cpython-313.pyc +0 -0
  284. package/omega/Agentik_Engine/tests/__pycache__/test_reducer.cpython-313-pytest-8.4.2.pyc +0 -0
  285. package/omega/Agentik_Engine/tests/__pycache__/test_report.cpython-313-pytest-8.4.2.pyc +0 -0
  286. package/omega/Agentik_Engine/tests/__pycache__/test_role_aliases_and_ssot.cpython-313-pytest-8.4.2.pyc +0 -0
  287. package/omega/Agentik_Engine/tests/__pycache__/test_role_aliases_and_ssot.cpython-313.pyc +0 -0
  288. package/omega/Agentik_Engine/tests/__pycache__/test_skill_discovery_and_gate.cpython-313-pytest-8.4.2.pyc +0 -0
  289. package/omega/Agentik_Engine/tests/__pycache__/test_skill_discovery_and_gate.cpython-313.pyc +0 -0
  290. package/omega/Agentik_Engine/tests/__pycache__/test_skill_power.cpython-313-pytest-8.4.2.pyc +0 -0
  291. package/omega/Agentik_Engine/tests/__pycache__/test_skill_power.cpython-313.pyc +0 -0
  292. package/omega/Agentik_Engine/tests/__pycache__/test_skill_routing.cpython-313-pytest-8.4.2.pyc +0 -0
  293. package/omega/Agentik_Engine/tests/__pycache__/test_skill_routing.cpython-313.pyc +0 -0
  294. package/omega/Agentik_Engine/tests/__pycache__/test_snapshot_partial.cpython-313-pytest-8.4.2.pyc +0 -0
  295. package/omega/Agentik_Engine/tests/__pycache__/test_snapshot_partial.cpython-313.pyc +0 -0
  296. package/omega/Agentik_Engine/tests/__pycache__/test_telegram_history.cpython-313-pytest-8.4.2.pyc +0 -0
  297. package/omega/Agentik_Engine/tests/__pycache__/test_telegram_history.cpython-313.pyc +0 -0
  298. package/omega/Agentik_Engine/tests/__pycache__/test_tmux_and_aisb_chat.cpython-313-pytest-8.4.2.pyc +0 -0
  299. package/omega/Agentik_Engine/tests/__pycache__/test_tmux_and_aisb_chat.cpython-313.pyc +0 -0
  300. package/omega/Agentik_Engine/tests/__pycache__/test_tools_and_sync.cpython-313-pytest-8.4.2.pyc +0 -0
  301. package/omega/Agentik_Engine/tests/__pycache__/test_tools_and_sync.cpython-313.pyc +0 -0
  302. package/omega/Agentik_Engine/tests/__pycache__/test_v06_features.cpython-313-pytest-8.4.2.pyc +0 -0
  303. package/omega/Agentik_Engine/tests/__pycache__/test_v06_features.cpython-313.pyc +0 -0
  304. package/omega/Agentik_Engine/tests/__pycache__/test_vault.cpython-313-pytest-8.4.2.pyc +0 -0
  305. package/omega/Agentik_Engine/tests/__pycache__/test_vault.cpython-313.pyc +0 -0
  306. package/omega/Agentik_Engine/tests/__pycache__/test_webhooks_and_readiness.cpython-313-pytest-8.4.2.pyc +0 -0
  307. package/omega/Agentik_Engine/tests/__pycache__/test_webhooks_and_readiness.cpython-313.pyc +0 -0
  308. package/omega/Agentik_Engine/tests/__pycache__/test_worker_and_cleanup.cpython-313-pytest-8.4.2.pyc +0 -0
  309. package/omega/Agentik_Engine/tests/__pycache__/test_worker_and_cleanup.cpython-313.pyc +0 -0
  310. package/omega/Agentik_Engine/tests/test_account.py +338 -0
  311. package/omega/Agentik_Engine/tests/test_adversarial.py +351 -0
  312. package/omega/Agentik_Engine/tests/test_agents_envelope.py +274 -0
  313. package/omega/Agentik_Engine/tests/test_audits_pipeline.py +348 -0
  314. package/omega/Agentik_Engine/tests/test_auto_update_and_migrations.py +394 -0
  315. package/omega/Agentik_Engine/tests/test_autonomous.py +361 -0
  316. package/omega/Agentik_Engine/tests/test_educators.py +233 -0
  317. package/omega/Agentik_Engine/tests/test_genesis_and_plan.py +573 -0
  318. package/omega/Agentik_Engine/tests/test_graphify.py +190 -0
  319. package/omega/Agentik_Engine/tests/test_handoff.py +311 -0
  320. package/omega/Agentik_Engine/tests/test_hermes_and_ua.py +387 -0
  321. package/omega/Agentik_Engine/tests/test_hermes_bootstrap_and_desktop.py +358 -0
  322. package/omega/Agentik_Engine/tests/test_install_steps.py +359 -0
  323. package/omega/Agentik_Engine/tests/test_install_ux.py +151 -0
  324. package/omega/Agentik_Engine/tests/test_installer_wiring.py +496 -0
  325. package/omega/Agentik_Engine/tests/test_intelligence.py +285 -0
  326. package/omega/Agentik_Engine/tests/test_llm_clis_and_uninstall.py +228 -0
  327. package/omega/Agentik_Engine/tests/test_managed_agent.py +363 -0
  328. package/omega/Agentik_Engine/tests/test_max_provider_and_menu.py +231 -0
  329. package/omega/Agentik_Engine/tests/test_menu_coverage.py +72 -0
  330. package/omega/Agentik_Engine/tests/test_pursue_cadence.py +217 -0
  331. package/omega/Agentik_Engine/tests/test_rag.py +287 -0
  332. package/omega/Agentik_Engine/tests/test_role_aliases_and_ssot.py +207 -0
  333. package/omega/Agentik_Engine/tests/test_skill_discovery_and_gate.py +337 -0
  334. package/omega/Agentik_Engine/tests/test_skill_power.py +259 -0
  335. package/omega/Agentik_Engine/tests/test_skill_routing.py +189 -0
  336. package/omega/Agentik_Engine/tests/test_snapshot_partial.py +172 -0
  337. package/omega/Agentik_Engine/tests/test_telegram_history.py +209 -0
  338. package/omega/Agentik_Engine/tests/test_tmux_and_aisb_chat.py +223 -0
  339. package/omega/Agentik_Engine/tests/test_tools_and_sync.py +312 -0
  340. package/omega/Agentik_Engine/tests/test_v06_features.py +370 -0
  341. package/omega/Agentik_Engine/tests/test_vault.py +173 -0
  342. package/omega/Agentik_Engine/tests/test_webhooks_and_readiness.py +277 -0
  343. package/omega/Agentik_Engine/tests/test_worker_and_cleanup.py +541 -0
  344. package/omega/Agentik_Extra/etc/secrets/.vault-key +3 -0
  345. package/omega/Agentik_Extra/etc/secrets/.vault-pub +1 -0
  346. package/omega/Agentik_Runtime/audits.db +0 -0
  347. package/omega/Agentik_SSOT/VERSION +1 -1
  348. package/omega/Agentik_SSOT/claude-plugins/claude-plugins.yaml +100 -0
  349. package/omega/Agentik_SSOT/docs/LAYERS.md +90 -0
  350. package/omega/Agentik_SSOT/docs/USER-JOURNEY.md +283 -0
  351. package/omega/Agentik_SSOT/marketplaces/design-discipline.yaml +86 -0
  352. package/omega/Agentik_SSOT/skills/a11yaudit/SKILL.md +161 -0
  353. package/omega/Agentik_SSOT/skills/apiaudit/SKILL.md +157 -0
  354. package/omega/Agentik_SSOT/skills/automationaudit/SKILL.md +161 -0
  355. package/omega/Agentik_SSOT/skills/cadence/SKILL.md +76 -0
  356. package/omega/Agentik_SSOT/skills/codeaudit/SKILL.md +153 -0
  357. package/omega/Agentik_SSOT/skills/copyaudit/SKILL.md +161 -0
  358. package/omega/Agentik_SSOT/skills/dataaudit/SKILL.md +157 -0
  359. package/omega/Agentik_SSOT/skills/debugaudit/SKILL.md +161 -0
  360. package/omega/Agentik_SSOT/skills/dispatch/SKILL.md +79 -0
  361. package/omega/Agentik_SSOT/skills/dxaudit/SKILL.md +161 -0
  362. package/omega/Agentik_SSOT/skills/featureaudit/SKILL.md +161 -0
  363. package/omega/Agentik_SSOT/skills/flowaudit/SKILL.md +165 -0
  364. package/omega/Agentik_SSOT/skills/genesis/SKILL.md +116 -0
  365. package/omega/Agentik_SSOT/skills/handoff/SKILL.md +117 -0
  366. package/omega/Agentik_SSOT/skills/logicaudit/SKILL.md +165 -0
  367. package/omega/Agentik_SSOT/skills/motionaudit/SKILL.md +165 -0
  368. package/omega/Agentik_SSOT/skills/perfaudit/SKILL.md +161 -0
  369. package/omega/Agentik_SSOT/skills/plan/SKILL.md +127 -0
  370. package/omega/Agentik_SSOT/skills/pursue/SKILL.md +68 -0
  371. package/omega/Agentik_SSOT/skills/rag-route.md +82 -0
  372. package/omega/Agentik_SSOT/skills/refontaudit/SKILL.md +165 -0
  373. package/omega/Agentik_SSOT/skills/retentionaudit/SKILL.md +165 -0
  374. package/omega/Agentik_SSOT/skills/secaudit/SKILL.md +157 -0
  375. package/omega/Agentik_SSOT/skills/seoaudit/SKILL.md +161 -0
  376. package/omega/Agentik_SSOT/skills/skill-auditor/SKILL.md +83 -0
  377. package/omega/Agentik_SSOT/skills/skill-finder/SKILL.md +116 -0
  378. package/omega/Agentik_SSOT/skills/uiuxaudit/SKILL.md +165 -0
  379. package/package.json +2 -2
@@ -1,13 +1,16 @@
1
1
  """The `omega` command-line interface.
2
2
 
3
3
  A thin entry point. The installer's doctor step calls `omega doctor`; operators
4
- use `omega status` to see every task's derived state.
4
+ use `omega status` to see every task's derived state. `omega account ...` and
5
+ `omega billing` give visibility into the Claude Code Max account pool — see
6
+ `docs/ACCOUNT-AND-BILLING.md` for the design.
5
7
  """
6
8
  from __future__ import annotations
7
9
 
8
10
  import argparse
9
11
  import os
10
12
  import sys
13
+ import time
11
14
  from pathlib import Path
12
15
 
13
16
  from omega_engine import __version__
@@ -22,24 +25,612 @@ def cmd_version(_args: argparse.Namespace) -> int:
22
25
  return 0
23
26
 
24
27
 
25
- def cmd_doctor(_args: argparse.Namespace) -> int:
26
- """Validate an Omega OS deployment: the 8-block tree + the event store."""
28
+ def cmd_doctor(args: argparse.Namespace) -> int:
29
+ """Validate an Omega OS deployment end-to-end.
30
+
31
+ Sections checked in order:
32
+ 1. The 8-block tree (structural — fatal if missing)
33
+ 2. The event store
34
+ 3. The vault (backend + counts)
35
+ 4. Provider router config + per-provider credentials
36
+ 5. MCP catalog + installed tools
37
+ 6. Account pool entries vs vault entries
38
+ 7. Educator subsystem — the 8 self-improving educators load
39
+ 8. Autonomous charters parseable
40
+ 9. 24/7 services (systemd active state on Linux; launchctl on macOS)
41
+ 10. Telegram bridge reachable (if a token is in the vault)
42
+
43
+ Each line is `[ok]`, `[warn]`, or `[FAIL]`. Exit code is 0 iff nothing
44
+ FAILed. `warn` does not flip the exit; it surfaces upgrade opportunities.
45
+
46
+ With ``--json``, prints a single JSON object instead of pretty text —
47
+ suitable for monitors and CI.
48
+ """
49
+ import os as _os
50
+ import shutil as _sh
51
+ import subprocess as _sp
52
+
53
+ json_mode: bool = bool(getattr(args, "json", False))
27
54
  home = _omega_home()
55
+ results: list[dict[str, str]] = []
56
+ if not json_mode:
57
+ print(f"omega doctor — OMEGA_HOME={home}")
58
+ any_fail = False
59
+ any_warn = False
60
+ current_section = "general"
61
+
62
+ def section(name: str) -> None:
63
+ nonlocal current_section
64
+ current_section = name
65
+ if not json_mode:
66
+ print(f" -- {name} --")
67
+
68
+ def line(status: str, msg: str) -> None:
69
+ nonlocal any_fail, any_warn
70
+ if status == "FAIL":
71
+ any_fail = True
72
+ elif status == "warn":
73
+ any_warn = True
74
+ results.append({"section": current_section, "status": status, "msg": msg})
75
+ if not json_mode:
76
+ print(f" [{status}] {msg}")
77
+
78
+ # 1. 8-block structure
79
+ section("structure")
28
80
  blocks = [
29
81
  "Agentik_SSOT", "Agentik_Engine", "Agentik_Orchestration",
30
82
  "Agentik_Providers", "Agentik_Coding", "Agentik_Tools",
31
83
  "Agentik_Runtime", "Agentik_Extra",
32
84
  ]
33
- ok = True
34
- print(f"omega doctor — OMEGA_HOME={home}")
35
85
  for b in blocks:
36
- present = (home / b).is_dir()
37
- print(f" [{'ok' if present else 'MISSING'}] {b}")
38
- ok = ok and present
86
+ line("ok" if (home / b).is_dir() else "FAIL", f"block {b}")
87
+
88
+ # 2. Event store
89
+ section("event store")
39
90
  store = home / "Agentik_Runtime" / "eventlog" / "omega.db"
40
- print(f" [{'ok' if store.exists() else 'pending'}] event store: {store}")
41
- print("doctor: PASS" if ok else "doctor: FAIL")
42
- return 0 if ok else 1
91
+ line("ok" if store.exists() else "warn",
92
+ f"event store: {store}{' (none yet — run a mission)' if not store.exists() else ''}")
93
+
94
+ # 3. Vault
95
+ section("vault")
96
+ try:
97
+ from omega_engine.vault import vault_status
98
+ vs = vault_status(home)
99
+ if vs["backend"] == "age":
100
+ line("ok", f"vault backend: age ({vs['encrypted_count']} encrypted)")
101
+ else:
102
+ line("warn",
103
+ f"vault backend: plain ({vs['plaintext_count']} files in chmod 600)"
104
+ + (" — install `age` for encryption" if not vs.get("age_installed") else ""))
105
+ if vs.get("plaintext_count", 0) > 0 and vs["backend"] == "age":
106
+ line("warn",
107
+ f"vault has {vs['plaintext_count']} unencrypted secret(s) left over — "
108
+ "re-write them through `omega account login` / `omega tool install`")
109
+ placeholders = vs.get("placeholder_count", 0)
110
+ if placeholders:
111
+ line("ok",
112
+ f"{placeholders} placeholder dotenv(s) await operator input (no leak)")
113
+ except Exception as exc: # noqa: BLE001
114
+ line("warn", f"vault status unavailable: {exc}")
115
+
116
+ # 4. Providers router + credentials
117
+ section("providers")
118
+ router_path = home / "Agentik_SSOT" / "providers" / "router.yaml"
119
+ if router_path.exists():
120
+ try:
121
+ import yaml as _yaml
122
+ cfg = _yaml.safe_load(router_path.read_text()) or {}
123
+ for p in cfg.get("providers") or []:
124
+ pid = p.get("id", "?")
125
+ roles = p.get("roles") or []
126
+ secret_ref = p.get("secret_ref") or f"{pid.upper()}_API_KEY"
127
+ has_env = bool(_os.environ.get(secret_ref))
128
+ try:
129
+ from omega_engine.vault import vault_read
130
+ has_vault = bool(vault_read(home, secret_ref))
131
+ except Exception:
132
+ has_vault = False
133
+ creds = "env" if has_env else ("vault" if has_vault else "missing")
134
+ stat = "ok" if creds != "missing" else "warn"
135
+ line(stat, f"provider {pid} (roles={roles}, creds={creds})")
136
+ except Exception as exc: # noqa: BLE001
137
+ line("FAIL", f"provider router unreadable: {exc}")
138
+ else:
139
+ line("warn", f"no provider router at {router_path} (none in manifest, or step 35 skipped)")
140
+
141
+ # 5. MCP catalog + installed tools
142
+ section("mcp")
143
+ catalog = home / "Agentik_SSOT" / "mcp" / "mcp-catalog.yaml"
144
+ config = home / "Agentik_SSOT" / "mcp" / "mcp-config.yaml"
145
+ line("ok" if catalog.exists() else "FAIL", f"MCP catalog: {catalog.name}")
146
+ if config.exists():
147
+ try:
148
+ import yaml as _yaml
149
+ cfg = _yaml.safe_load(config.read_text()) or {}
150
+ entries = cfg.get("mcp") or cfg.get("servers") or []
151
+ line("ok", f"MCP config: {len(entries)} server(s) registered")
152
+ except Exception as exc: # noqa: BLE001
153
+ line("warn", f"MCP config unreadable: {exc}")
154
+ else:
155
+ line("warn", "no MCP config yet (run `omega tool install <id>` or re-run step 40)")
156
+
157
+ # 6. Account pool vs vault
158
+ section("accounts")
159
+ try:
160
+ from omega_engine.account import AccountPool
161
+ from omega_engine.vault import vault_read
162
+ pool = AccountPool.load(home)
163
+ if not pool.accounts:
164
+ line("warn", "account pool is empty (run `omega account login`)")
165
+ for a in pool.accounts:
166
+ ref = a.secret_ref
167
+ has_token = False
168
+ try:
169
+ has_token = bool(vault_read(home, f"{ref}_TOKEN") or vault_read(home, ref))
170
+ except Exception:
171
+ has_token = False
172
+ # `disabled` accounts shouldn't be expected to have a token; for
173
+ # `active`/`resting` ones, missing token is a soft warning — the
174
+ # operator just hasn't run `omega account login` yet.
175
+ if has_token:
176
+ line("ok", f"account {a.id} ({a.status}) — token in vault")
177
+ else:
178
+ line("warn",
179
+ f"account {a.id} ({a.status}) — no token yet "
180
+ f"(run `omega account login --id {a.id}`)")
181
+ except Exception as exc: # noqa: BLE001
182
+ line("warn", f"account pool unreadable: {exc}")
183
+
184
+ # 6b. Agentik OS native skills (replacements for /goal + /loop on Max)
185
+ section("agentik skills")
186
+ skills_dir = home / "Agentik_SSOT" / "skills"
187
+ expected_skills = {"pursue", "cadence", "dispatch"}
188
+ found_skills: set[str] = set()
189
+ if skills_dir.is_dir():
190
+ for child in skills_dir.iterdir():
191
+ if child.is_dir() and (child / "SKILL.md").exists():
192
+ found_skills.add(child.name)
193
+ elif child.suffix == ".md":
194
+ found_skills.add(child.stem)
195
+ missing = expected_skills - found_skills
196
+ if missing:
197
+ line("warn", f"missing Agentik OS skills: {sorted(missing)}")
198
+ else:
199
+ line("ok", f"Agentik OS native skills present: {sorted(expected_skills)}")
200
+ if found_skills - expected_skills:
201
+ extras = sorted(found_skills - expected_skills)
202
+ line("ok", f"plus operator skills: {extras[:5]}{' (+more)' if len(extras) > 5 else ''}")
203
+
204
+ # 6b2. Semantic memory + learning loop
205
+ section("memory + learning")
206
+ try:
207
+ from omega_engine import memory as _mem
208
+ stats = _mem.index_stats(home)
209
+ if stats["indexed"]:
210
+ line("ok", f"semantic memory: {stats['total']:,d} docs indexed")
211
+ else:
212
+ line("warn",
213
+ "semantic memory not indexed — run `omega memory reindex`")
214
+ except Exception as exc: # noqa: BLE001
215
+ line("warn", f"memory stats failed: {exc}")
216
+ # Learning loop — count proposals on disk
217
+ try:
218
+ staging = home / "Agentik_Extra" / "staging" / "promotion"
219
+ if staging.is_dir():
220
+ proposals = list(staging.glob("smith-*.json"))
221
+ if proposals:
222
+ line("ok",
223
+ f"{len(proposals)} Smith reflection(s) pending in staging")
224
+ else:
225
+ line("ok",
226
+ "no Smith reflections pending "
227
+ "(run `omega learn reflect --write`)")
228
+ except Exception as exc: # noqa: BLE001
229
+ line("warn", f"learning scan failed: {exc}")
230
+
231
+ # 6b3. Tmux orchestration — categorised session population
232
+ section("tmux")
233
+ try:
234
+ from omega_engine import tmux as _tx
235
+ report = _tx.health_report(home)
236
+ if not report.get("tmux_installed"):
237
+ line("FAIL", "tmux not on PATH — run `omega upgrade` to reinstall")
238
+ else:
239
+ n = report["sessions"]
240
+ cats = report.get("by_category", {})
241
+ if n == 0:
242
+ line("ok", "tmux installed, no sessions yet "
243
+ "(omega tmux spawn ...)")
244
+ else:
245
+ line("ok", f"tmux: {n} session(s), categories: "
246
+ + ", ".join(f"{c}={len(v)}"
247
+ for c, v in cats.items() if v))
248
+ if report.get("stale"):
249
+ line("warn",
250
+ f"{len(report['stale'])} stale worker session(s) >6h "
251
+ "detached — run `omega tmux cleanup`")
252
+ except Exception as exc: # noqa: BLE001
253
+ line("warn", f"tmux health check failed: {exc}")
254
+
255
+ # 6c0. Provider authentication mode — Max OAuth via `claude` CLI vs API key
256
+ section("llm authentication")
257
+ try:
258
+ from omega_engine.provider import ClaudeMaxProvider
259
+ if ClaudeMaxProvider.is_available():
260
+ line("ok",
261
+ "Claude Code Max — using OAuth via `claude` CLI subprocess "
262
+ "(no API key needed; cost tracked in $)")
263
+ elif os.environ.get("ANTHROPIC_API_KEY"):
264
+ line("warn",
265
+ "ANTHROPIC_API_KEY set — falling back to API (paid per call). "
266
+ "Install `claude` CLI + `claude /login` to use your Max subscription instead.")
267
+ else:
268
+ line("warn",
269
+ "No `claude` CLI and no ANTHROPIC_API_KEY — engine will use "
270
+ "the MockProvider. Install Claude Code + log in to use Max.")
271
+ except Exception as exc: # noqa: BLE001
272
+ line("warn", f"provider auth check failed: {exc}")
273
+
274
+ # 6c1. LLM CLIs installed on the host
275
+ section("llm clis")
276
+ import shutil as _shu
277
+ import subprocess as _sup
278
+ _cli_specs = [
279
+ ("claude_code", "claude", ["claude", "--version"]),
280
+ ("gemini_cli", "gemini", ["gemini", "--version"]),
281
+ ("codex", "codex", ["codex", "--version"]),
282
+ ("aider", "aider", ["aider", "--version"]),
283
+ ]
284
+ any_cli = False
285
+ for cli_id, bin_name, ver_cmd in _cli_specs:
286
+ if _shu.which(bin_name):
287
+ any_cli = True
288
+ try:
289
+ proc = _sup.run(ver_cmd, check=False, capture_output=True,
290
+ text=True, timeout=15)
291
+ v = (proc.stdout or proc.stderr or "(unknown)").strip().splitlines()[0][:60]
292
+ except Exception: # noqa: BLE001
293
+ v = "(version check failed)"
294
+ line("ok", f"{cli_id:<14} {v}")
295
+ else:
296
+ line("warn", f"{cli_id:<14} (not on PATH)")
297
+ # GLM SDK is a Python import, not a binary
298
+ try:
299
+ import importlib
300
+ importlib.import_module("zhipuai")
301
+ line("ok", "glm_sdk (zhipuai Python SDK importable)")
302
+ any_cli = True
303
+ except ImportError:
304
+ line("warn", "glm_sdk (not installed — `uv tool install zhipuai`)")
305
+ if not any_cli:
306
+ line("warn", "no LLM CLI installed — `omega upgrade` or re-run "
307
+ "install.sh with the llm_clis: block")
308
+
309
+ # 6c2. Claude Code settings — Agent Teams + bypass + Stop hook
310
+ section("claude code settings")
311
+ import json as _json
312
+ settings_path = Path.home() / ".claude" / "settings.json"
313
+ if not settings_path.exists():
314
+ line("warn",
315
+ f"no settings.json at {settings_path} — Agent Teams + audit gate not active "
316
+ "(re-run install step 33-claude-code-settings)")
317
+ else:
318
+ try:
319
+ data = _json.loads(settings_path.read_text())
320
+ env_at = (data.get("env") or {}).get("CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS")
321
+ line("ok" if env_at == "1" else "warn",
322
+ f"Agent Teams flag: {env_at or 'unset'}")
323
+ dm = (data.get("permissions") or {}).get("defaultMode")
324
+ line("ok" if dm == "bypassPermissions" else "warn",
325
+ f"permissions.defaultMode: {dm or 'default'}")
326
+ stop_hooks = (data.get("hooks") or {}).get("Stop") or []
327
+ has_gate = any(
328
+ isinstance(e, dict) and e.get("tag") == "omega-audit-gate"
329
+ for e in stop_hooks
330
+ )
331
+ line("ok" if has_gate else "warn",
332
+ f"Stop-hook audit gate: {'wired' if has_gate else 'not wired'}")
333
+ except Exception as exc: # noqa: BLE001
334
+ line("warn", f"settings.json unreadable: {exc}")
335
+
336
+ # 6c5. Inbound webhooks — gate ready?
337
+ section("webhooks")
338
+ try:
339
+ from omega_engine.webhooks import load_registry
340
+ reg = load_registry(home)
341
+ if not reg.specs:
342
+ line("ok", "no webhooks configured (optional)")
343
+ else:
344
+ for s in reg.specs:
345
+ from omega_engine.webhooks import _load_secret
346
+ has_secret = bool(_load_secret(home, s.secret_ref))
347
+ stat = "ok" if has_secret else "warn"
348
+ line(stat,
349
+ f"{s.id:<20} path={s.path} source={s.source} "
350
+ f"secret={'set' if has_secret else 'MISSING'}")
351
+ except Exception as exc: # noqa: BLE001
352
+ line("warn", f"webhook registry unreadable: {exc}")
353
+
354
+ # 6d. Audit health — last score + trend per audit
355
+ section("audit history")
356
+ try:
357
+ from omega_engine.audit_arsenal import AuditRegistry
358
+ from omega_engine.audits.history import latest_score, trend
359
+ registry = AuditRegistry.load(home / "Agentik_SSOT" / "audits")
360
+ any_run = False
361
+ for audit in registry.all():
362
+ score = latest_score(home, audit.id)
363
+ if score is None:
364
+ continue
365
+ any_run = True
366
+ t = trend(home, audit.id)
367
+ stat = "ok" if score >= audit.threshold else "warn"
368
+ line(stat,
369
+ f"{audit.id:<18} latest={score:>3} {t} "
370
+ f"(threshold {audit.threshold})")
371
+ if not any_run:
372
+ line("ok", "no audit runs recorded yet "
373
+ "(run `omega audit all` to seed history)")
374
+ except Exception as exc: # noqa: BLE001
375
+ line("warn", f"audit history check failed: {exc}")
376
+
377
+ # 6c. Cadence charters scheduled
378
+ try:
379
+ from omega_engine.cadence import list_cadences
380
+ cads = list_cadences(home)
381
+ if cads:
382
+ on = [c for c in cads if c["enabled"]]
383
+ line("ok", f"{len(on)}/{len(cads)} cadence(s) scheduled and enabled")
384
+ except Exception as exc: # noqa: BLE001
385
+ line("warn", f"cadence scan failed: {exc}")
386
+
387
+ # 7a. AISB agent suite
388
+ section("aisb suite")
389
+ try:
390
+ from omega_engine.prompts import list_available_agents
391
+ suites = list_available_agents(home)
392
+ if not suites:
393
+ line("warn",
394
+ f"no agent suites at {home}/Agentik_SSOT/agents/ — "
395
+ "step 25-aisb-suite may have been skipped")
396
+ else:
397
+ for suite, roles in suites.items():
398
+ line("ok", f"suite '{suite}': {len(roles)} agents ({', '.join(roles[:5])}{'...' if len(roles) > 5 else ''})")
399
+ # Specifically check the 13 AISB agents we expect.
400
+ expected_aisb = {
401
+ "architect", "construct", "keymaker", "link", "merovingian",
402
+ "morpheus", "neo", "niobe", "oracle", "pythia", "seraph",
403
+ "smith", "zion",
404
+ }
405
+ actual_aisb = set(suites.get("aisb", []))
406
+ missing = expected_aisb - actual_aisb
407
+ if missing:
408
+ line("warn", f"AISB suite incomplete — missing: {sorted(missing)}")
409
+ else:
410
+ line("ok", "AISB suite complete (13/13 agents)")
411
+ except Exception as exc: # noqa: BLE001
412
+ line("FAIL", f"agent suite check failed: {exc}")
413
+
414
+ # 7b. Per-project vaults
415
+ section("project vaults")
416
+ try:
417
+ from omega_engine.vault import list_project_vaults
418
+ vaults = list_project_vaults(home)
419
+ if not vaults:
420
+ line("ok", "no per-project vaults yet (none required)")
421
+ for v in vaults:
422
+ backend = v.get("backend", "?")
423
+ ec = v.get("encrypted_count", 0)
424
+ pc = v.get("plaintext_count", 0)
425
+ stat = "ok"
426
+ if backend != "age" and (ec + pc) > 0:
427
+ stat = "warn"
428
+ line(stat,
429
+ f"project '{v.get('project')}': backend={backend}, "
430
+ f"encrypted={ec}, plaintext={pc}")
431
+ except Exception as exc: # noqa: BLE001
432
+ line("warn", f"project vault scan failed: {exc}")
433
+
434
+ # 7. Educators
435
+ section("educators")
436
+ try:
437
+ from omega_engine import educators as _edu
438
+ expected = [
439
+ "PromptEducator", "ArtifactEducator", "SkillEducator",
440
+ "CoworkerEducator", "ConnectionEducator", "AutomationEducator",
441
+ "ClaudecodeEducator", "LoopEducator",
442
+ ]
443
+ missing = [c for c in expected if not hasattr(_edu, c)]
444
+ if missing:
445
+ line("FAIL", f"missing educator classes: {missing}")
446
+ else:
447
+ line("ok", f"{len(expected)} educators loadable")
448
+ staging = home / "Agentik_Extra" / "staging" / "promotion"
449
+ if staging.is_dir():
450
+ line("ok", f"staging pipeline at {staging}")
451
+ else:
452
+ line("warn", f"no staging pipeline dir at {staging}")
453
+ except Exception as exc: # noqa: BLE001
454
+ line("FAIL", f"educators module unloadable: {exc}")
455
+
456
+ # 8. Autonomous charters
457
+ section("autonomous charters")
458
+ chdir = home / "Agentik_Orchestration" / "autonomous"
459
+ if chdir.is_dir():
460
+ charters = sorted(chdir.glob("*.yaml"))
461
+ if not charters:
462
+ line("warn", f"no charters at {chdir}")
463
+ else:
464
+ import yaml as _yaml
465
+ for c in charters:
466
+ try:
467
+ data = _yaml.safe_load(c.read_text()) or {}
468
+ cid = data.get("id") or c.stem
469
+ enabled = bool(data.get("enabled", True))
470
+ trigger = (data.get("trigger") or {}).get("type", "?")
471
+ line("ok", f"charter {cid} (trigger={trigger}, enabled={enabled})")
472
+ except Exception as exc: # noqa: BLE001
473
+ line("FAIL", f"charter {c.name} unparseable: {exc}")
474
+ else:
475
+ line("warn", f"no autonomous dir at {chdir}")
476
+
477
+ # 9. 24/7 services
478
+ section("24/7 services")
479
+ import platform as _platform
480
+ if _platform.system() == "Linux":
481
+ unitdir = Path.home() / ".config" / "systemd" / "user"
482
+ units_installed = (unitdir / "omega-engine.service").exists()
483
+ if _sh.which("systemctl") is None:
484
+ line("warn", "systemctl not on PATH — services step skipped")
485
+ elif not units_installed:
486
+ line("warn",
487
+ f"no service units at {unitdir} — re-run install.sh "
488
+ "with a non-minimal profile to enable the 24/7 layer")
489
+ else:
490
+ for unit in ("omega-engine", "omega-telegram", "omega-autonomous"):
491
+ proc = _sp.run(
492
+ ["systemctl", "--user", "is-active", unit],
493
+ check=False, capture_output=True, text=True, timeout=10,
494
+ )
495
+ state = (proc.stdout or proc.stderr or "").strip()
496
+ if state == "active":
497
+ line("ok", f"systemd: {unit} (active)")
498
+ elif state in {"inactive", "failed"}:
499
+ line("FAIL", f"systemd: {unit} ({state}) — unit exists, "
500
+ "start with `systemctl --user start omega-*`")
501
+ else:
502
+ line("warn", f"systemd: {unit} ({state or 'unknown'})")
503
+ elif _platform.system() == "Darwin":
504
+ agentdir = Path.home() / "Library" / "LaunchAgents"
505
+ plists_installed = (agentdir / "com.agentikos.omega.engine.plist").exists()
506
+ if _sh.which("launchctl") is None:
507
+ line("warn", "launchctl not on PATH")
508
+ elif not plists_installed:
509
+ line("warn",
510
+ f"no LaunchAgent plists at {agentdir} — re-run install.sh "
511
+ "with a non-minimal profile to enable the 24/7 layer")
512
+ else:
513
+ for label in ("com.agentikos.omega.engine",
514
+ "com.agentikos.omega.telegram",
515
+ "com.agentikos.omega.autonomous"):
516
+ proc = _sp.run(
517
+ ["launchctl", "list", label],
518
+ check=False, capture_output=True, text=True, timeout=10,
519
+ )
520
+ if proc.returncode == 0:
521
+ line("ok", f"launchd: {label} (loaded)")
522
+ else:
523
+ line("FAIL", f"launchd: {label} (plist present but not loaded) "
524
+ "— `launchctl bootstrap gui/$(id -u) <plist>`")
525
+ else:
526
+ line("warn", f"unsupported OS for services check: {_platform.system()}")
527
+
528
+ # 10. Telegram bridge
529
+ section("telegram")
530
+ try:
531
+ from omega_engine.vault import vault_read
532
+ token = vault_read(home, "TELEGRAM_TOKEN") or None
533
+ except Exception:
534
+ token = None
535
+ if not token:
536
+ # Backward compat: read from the legacy telegram.env file.
537
+ legacy = home / "Agentik_Extra" / "etc" / "secrets" / "telegram.env"
538
+ if legacy.exists():
539
+ for ln in legacy.read_text().splitlines():
540
+ if ln.startswith("TELEGRAM_TOKEN="):
541
+ token = ln.split("=", 1)[1].strip()
542
+ break
543
+ if not token:
544
+ line("warn", "no Telegram token in vault (skip if minimal profile)")
545
+ else:
546
+ try:
547
+ import urllib.request as _ur
548
+ req = _ur.Request(f"https://api.telegram.org/bot{token}/getMe")
549
+ with _ur.urlopen(req, timeout=10) as resp: # noqa: S310
550
+ body = resp.read().decode("utf-8", errors="replace")
551
+ if '"ok":true' in body:
552
+ line("ok", "telegram bot reachable (getMe → ok)")
553
+ else:
554
+ line("FAIL", f"telegram getMe returned: {body[:120]}")
555
+ except Exception as exc: # noqa: BLE001
556
+ line("FAIL", f"telegram bot unreachable: {exc}")
557
+
558
+ # ────────────────────────────────────────────────────────────────────
559
+ # Ready / Partial / Not Ready verdict.
560
+ #
561
+ # CRITICAL: blocks block_count > 0 in these sections → NOT READY
562
+ # structure, event store, vault, providers, accounts, aisb suite,
563
+ # educators
564
+ # IMPORTANT: warns in these sections → PARTIAL (engine works but
565
+ # surfaces missing — usually just a profile thing)
566
+ # 24/7 services, telegram, llm clis, claude code settings,
567
+ # audit history
568
+ # OPTIONAL: anything else — flag but never demote
569
+ # ────────────────────────────────────────────────────────────────────
570
+ CRITICAL_SECTIONS = {
571
+ "structure", "event store", "vault", "providers",
572
+ "accounts", "aisb suite", "educators",
573
+ }
574
+ IMPORTANT_SECTIONS = {
575
+ "24/7 services", "telegram", "llm clis",
576
+ "claude code settings", "audit history", "mcp",
577
+ }
578
+ critical_fails: list[str] = []
579
+ important_warns: list[str] = []
580
+ for r in results:
581
+ if r["status"] == "FAIL" and r["section"] in CRITICAL_SECTIONS:
582
+ critical_fails.append(f"{r['section']}: {r['msg']}")
583
+ elif r["status"] == "FAIL":
584
+ # A FAIL outside CRITICAL still demotes to NOT READY — a real
585
+ # FAIL is a real FAIL.
586
+ critical_fails.append(f"{r['section']}: {r['msg']}")
587
+ elif r["status"] == "warn" and r["section"] in IMPORTANT_SECTIONS:
588
+ important_warns.append(r["section"])
589
+
590
+ if critical_fails:
591
+ readiness = "NOT_READY"
592
+ readiness_msg = (
593
+ f"{len(critical_fails)} critical failure(s) — engine cannot "
594
+ f"reliably run missions. First: {critical_fails[0][:200]}"
595
+ )
596
+ elif important_warns:
597
+ readiness = "PARTIAL"
598
+ unique = sorted(set(important_warns))
599
+ readiness_msg = (
600
+ f"engine ready, but surfaces missing: {', '.join(unique)}. "
601
+ "Re-run install to fill them in."
602
+ )
603
+ else:
604
+ readiness = "READY"
605
+ readiness_msg = "every surface healthy — ready to run missions."
606
+
607
+ verdict = "FAIL" if any_fail else ("warnings" if any_warn else "PASS")
608
+
609
+ if json_mode:
610
+ import json as _json
611
+ payload = {
612
+ "omega_home": str(home),
613
+ "verdict": verdict,
614
+ "readiness": readiness,
615
+ "readiness_msg": readiness_msg,
616
+ "any_fail": any_fail,
617
+ "any_warn": any_warn,
618
+ "results": results,
619
+ }
620
+ print(_json.dumps(payload, indent=2))
621
+ return 1 if any_fail else 0
622
+
623
+ print()
624
+ if any_fail:
625
+ print("doctor: FAIL")
626
+ elif any_warn:
627
+ print("doctor: PASS with warnings")
628
+ else:
629
+ print("doctor: PASS")
630
+ print()
631
+ icon = {"READY": "✓", "PARTIAL": "~", "NOT_READY": "✗"}[readiness]
632
+ print(f" {icon} {readiness} — {readiness_msg}")
633
+ return 1 if any_fail else 0
43
634
 
44
635
 
45
636
  def cmd_status(_args: argparse.Namespace) -> int:
@@ -62,43 +653,237 @@ def cmd_status(_args: argparse.Namespace) -> int:
62
653
  return 0
63
654
 
64
655
 
65
- def cmd_account(_args: argparse.Namespace) -> int:
66
- """Show the Claude Code Max account pool and the selection strategy.
656
+ # ──── account subcommands ─────────────────────────────────────────────────
657
+
658
+
659
+ def _print_pool(pool) -> None:
660
+ print(f"Claude Max account pool — selection: {pool.selection}")
661
+ if not pool.accounts:
662
+ print(" (empty)")
663
+ print(" add an account: omega account login")
664
+ return
665
+ for a in pool.accounts:
666
+ used = f"{a.tokens_used:>9,d} tok" if a.tokens_used else " -"
667
+ print(f" [{a.status:<9}] {a.id:<16} {used} {a.label}")
668
+ print(" add an account: omega account login")
669
+
670
+
671
+ def cmd_account_list(_args: argparse.Namespace) -> int:
672
+ """Show the Claude Code Max account pool and the selection strategy."""
673
+ from omega_engine.account import AccountPool
674
+
675
+ pool = AccountPool.load(_omega_home())
676
+ _print_pool(pool)
677
+ return 0
678
+
679
+
680
+ def cmd_account_login(args: argparse.Namespace) -> int:
681
+ """Add an account: run the OAuth flow, store the token in the vault,
682
+ and add a pool entry.
67
683
 
68
- Omega OS runs one engine, not N tmux sessions so an account is not
69
- "switched" globally. The Claude provider holds a POOL and distributes agent
70
- calls across accounts. Add one with `omega account login`.
71
- See docs/ACCOUNT-AND-BILLING.md.
684
+ Strategy: try the RFC 8628 device-code flow first; if it's unavailable
685
+ (Anthropic does not currently publish those endpoints), fall back to a
686
+ manual paste flow the user logs in via browser elsewhere, copies the
687
+ token, and pastes it. Either way ends with the token in the vault and
688
+ a new account in `accounts.yaml`.
72
689
  """
73
- cfg = _omega_home() / "Agentik_Providers" / "claude" / "accounts.yaml"
74
- if not cfg.exists():
75
- cfg = cfg.with_name("accounts.example.yaml")
76
- if not cfg.exists():
77
- print("no Claude Max account pool configured — see docs/ACCOUNT-AND-BILLING.md")
78
- return 0
690
+ from omega_engine.account import (
691
+ AccountPool,
692
+ ClaudeAccount,
693
+ claude_device_code_flow,
694
+ claude_oauth_login_url,
695
+ write_token,
696
+ )
697
+
698
+ account_id = (args.id or "").strip()
699
+ if not account_id:
700
+ account_id = input("account id (e.g. max-primary): ").strip()
701
+ if not account_id:
702
+ print("account id is required — aborting")
703
+ return 2
704
+ label = (args.label or "").strip() or f"Claude Max account ({account_id})"
705
+ secret_ref = f"CLAUDE_OAUTH_{account_id}"
706
+
707
+ token: str | None = None
708
+ if not args.manual:
709
+ print("attempting OAuth device-code flow...")
710
+ try:
711
+ result = claude_device_code_flow()
712
+ instruction = result.get("raw", {}).get("_human_instruction") \
713
+ or result.get("_human_instruction") or ""
714
+ if instruction:
715
+ print(instruction)
716
+ token = result.get("access_token")
717
+ except RuntimeError as exc:
718
+ print(f"device-code flow unavailable: {exc}")
719
+ print("falling back to manual paste...")
720
+
721
+ if not token:
722
+ # manual fallback — this is the path that always works
723
+ print()
724
+ print("MANUAL LOGIN")
725
+ print(f" 1. open in a browser: {claude_oauth_login_url()}")
726
+ print(" 2. complete login on the Claude account you want to add")
727
+ print(" 3. copy the OAuth token from the dashboard or developer panel")
728
+ print()
729
+ try:
730
+ token = input("paste the OAuth token here: ").strip()
731
+ except EOFError:
732
+ token = ""
733
+ if not token:
734
+ print("no token provided — aborting (vault not modified)")
735
+ return 2
736
+
737
+ home = _omega_home()
738
+ path = write_token(home, secret_ref, token)
739
+ pool = AccountPool.load(home)
740
+ pool.add(ClaudeAccount(
741
+ id=account_id, label=label, secret_ref=secret_ref,
742
+ weight=1, status="active",
743
+ ))
744
+ cfg = pool.save(home)
745
+ print(f" vault: {path} (chmod 600)")
746
+ print(f" pool: {cfg}")
747
+ _print_pool(pool)
748
+ return 0
749
+
750
+
751
+ def cmd_account_use(args: argparse.Namespace) -> int:
752
+ """Set an account's status: active | resting | disabled."""
753
+ from omega_engine.account import AccountPool
754
+
755
+ home = _omega_home()
756
+ pool = AccountPool.load(home)
757
+ if pool.get(args.id) is None:
758
+ print(f"no account '{args.id}' in the pool")
759
+ return 2
79
760
  try:
80
- import yaml
81
- except ImportError:
82
- print(f"account pool config: {cfg} (install pyyaml to render it)")
83
- return 0
84
- data = yaml.safe_load(cfg.read_text()) or {}
85
- pool = data.get("pool", [])
86
- print(f"Claude Max account pool — selection: {data.get('selection', 'least-used')}")
87
- for a in pool or [{"status": "(empty)", "id": "", "label": ""}]:
88
- print(f" [{str(a.get('status', '?')):<9}] {str(a.get('id', '')):<16} {a.get('label', '')}")
89
- print(" add an account: omega account login (docs/ACCOUNT-AND-BILLING.md)")
761
+ pool.set_status(args.id, args.status)
762
+ except ValueError as exc:
763
+ print(str(exc))
764
+ return 2
765
+ pool.save(home)
766
+ _print_pool(pool)
90
767
  return 0
91
768
 
92
769
 
770
+ def cmd_account_pool(args: argparse.Namespace) -> int:
771
+ """Edit pool-wide knobs: selection strategy and per-account weights.
772
+
773
+ Non-interactive flags drive scripted edits; with no flags, it prints the
774
+ current pool for inspection.
775
+ """
776
+ from omega_engine.account import AccountPool
777
+
778
+ home = _omega_home()
779
+ pool = AccountPool.load(home)
780
+ changed = False
781
+ if args.selection:
782
+ if args.selection not in ("round-robin", "least-used", "by-quota"):
783
+ print(
784
+ f"invalid selection '{args.selection}' — "
785
+ "must be round-robin|least-used|by-quota"
786
+ )
787
+ return 2
788
+ pool.selection = args.selection
789
+ changed = True
790
+ if args.weight:
791
+ # each --weight ID=N — parse and apply
792
+ for spec in args.weight:
793
+ if "=" not in spec:
794
+ print(f"weight spec '{spec}' must be of the form id=N")
795
+ return 2
796
+ ident, _, val = spec.partition("=")
797
+ ident, val = ident.strip(), val.strip()
798
+ acc = pool.get(ident)
799
+ if acc is None:
800
+ print(f"no account '{ident}' — skipping")
801
+ continue
802
+ try:
803
+ acc.weight = max(int(val), 0)
804
+ except ValueError:
805
+ print(f"weight for '{ident}' must be an integer, got {val!r}")
806
+ return 2
807
+ changed = True
808
+ if changed:
809
+ pool.save(home)
810
+ _print_pool(pool)
811
+ return 0
812
+
813
+
814
+ def cmd_account(args: argparse.Namespace) -> int:
815
+ """Account dispatcher — keeps `omega account` (no subcommand) working as a
816
+ list view, matches the original surface area."""
817
+ sub = getattr(args, "account_cmd", None) or "list"
818
+ fns = {
819
+ "list": cmd_account_list,
820
+ "login": cmd_account_login,
821
+ "use": cmd_account_use,
822
+ "pool": cmd_account_pool,
823
+ }
824
+ if sub not in fns:
825
+ print(f"unknown account subcommand: {sub}")
826
+ return 2
827
+ return fns[sub](args)
828
+
829
+
830
+ # ──── billing ─────────────────────────────────────────────────────────────
831
+
832
+
93
833
  def cmd_billing(_args: argparse.Namespace) -> int:
94
834
  """Show usage and cost per Claude Max account.
95
835
 
96
- Per-account billing reads token usage recorded on the event log against the
97
- provider cost model so you see which account is near its weekly limit.
836
+ Sources: token usage on `task.*` events (live aggregation via
837
+ `BillingAggregator`) plus the cached counter in `accounts.yaml`. The event
838
+ log is the source of truth; the cached counter is the convenience view.
98
839
  """
840
+ from omega_engine.account import AccountPool, BillingAggregator
841
+ from omega_engine.store import SQLiteStore
842
+
843
+ home = _omega_home()
844
+ pool = AccountPool.load(home)
99
845
  print("omega billing — usage per Claude Max account")
100
- print(" source: token usage on task.* events + the provider cost model")
101
- print(" build-out: live per-account aggregation — see docs/ACCOUNT-AND-BILLING.md")
846
+ print()
847
+ print(f" pool selection: {pool.selection}")
848
+ print(f" accounts in pool: {len(pool.accounts)}")
849
+ print()
850
+
851
+ db = home / "Agentik_Runtime" / "eventlog" / "omega.db"
852
+ if not db.exists():
853
+ print(" no event store yet — nothing to aggregate")
854
+ return 0
855
+ store = SQLiteStore(db)
856
+ totals = BillingAggregator.from_event_log(store)
857
+ if not totals:
858
+ print(" no per-account usage recorded in the event log yet")
859
+ print(" (events emit `usage.account_id` once the Claude provider "
860
+ "is wired)")
861
+ store.close()
862
+ return 0
863
+
864
+ pool_ids = {a.id for a in pool.accounts}
865
+ header = f" {'account':<16} {'calls':>7} {'input_tok':>12} {'output_tok':>12} {'total_tok':>12}"
866
+ print(header)
867
+ print(f" {'-' * 64}")
868
+ grand_in = grand_out = grand_calls = 0
869
+ for account_id, t in sorted(totals.items()):
870
+ marker = "" if account_id in pool_ids else " (unknown)"
871
+ total_tok = t["input_tokens"] + t["output_tokens"]
872
+ print(
873
+ f" {account_id:<16} {t['total_calls']:>7,d} "
874
+ f"{t['input_tokens']:>12,d} {t['output_tokens']:>12,d} "
875
+ f"{total_tok:>12,d}{marker}"
876
+ )
877
+ grand_in += t["input_tokens"]
878
+ grand_out += t["output_tokens"]
879
+ grand_calls += t["total_calls"]
880
+ print(f" {'-' * 64}")
881
+ print(
882
+ f" {'TOTAL':<16} {grand_calls:>7,d} "
883
+ f"{grand_in:>12,d} {grand_out:>12,d} "
884
+ f"{grand_in + grand_out:>12,d}"
885
+ )
886
+ store.close()
102
887
  return 0
103
888
 
104
889
 
@@ -107,7 +892,15 @@ def cmd_run(args: argparse.Namespace) -> int:
107
892
  verifies, and a whitepaper PDF report is produced."""
108
893
  from omega_engine.mission import run_mission
109
894
 
110
- outcome = run_mission(args.intent)
895
+ # Reject empty / whitespace-only intents — they used to "succeed" silently
896
+ # with a 100%-complete result, masking the bug where the operator's prompt
897
+ # was lost mid-pipeline.
898
+ intent = (args.intent or "").strip()
899
+ if not intent:
900
+ print("error: intent is empty — supply a non-blank mission description",
901
+ file=sys.stderr)
902
+ return 2
903
+ outcome = run_mission(intent)
111
904
  print(f"mission {outcome.result.mission_id}: "
112
905
  f"{outcome.result.final_state.value} ({outcome.progress_pct}%)")
113
906
  if outcome.report_pdf:
@@ -118,10 +911,15 @@ def cmd_run(args: argparse.Namespace) -> int:
118
911
  def cmd_project(args: argparse.Namespace) -> int:
119
912
  """Create a project — folder, registry entry, and (if configured) a bound
120
913
  Telegram topic."""
914
+ # Subcommand: `omega project secret ...`
915
+ sub = getattr(args, "project_cmd", None)
916
+ if sub == "secret":
917
+ return cmd_project_secret(args)
918
+
121
919
  from omega_engine.project import create_project
122
920
 
123
921
  telegram = None
124
- if not args.no_telegram:
922
+ if not getattr(args, "no_telegram", False):
125
923
  try:
126
924
  from omega_engine.telegram import TelegramBridge
127
925
  telegram = TelegramBridge.from_vault()
@@ -134,23 +932,3733 @@ def cmd_project(args: argparse.Namespace) -> int:
134
932
  return 0
135
933
 
136
934
 
137
- def main(argv: list[str] | None = None) -> int:
138
- parser = argparse.ArgumentParser(prog="omega", description="Omega OS control CLI")
139
- sub = parser.add_subparsers(dest="cmd", required=True)
140
- sub.add_parser("version", help="print the engine version").set_defaults(fn=cmd_version)
141
- sub.add_parser("doctor", help="validate the deployment").set_defaults(fn=cmd_doctor)
142
- sub.add_parser("status", help="show all tasks and their derived state").set_defaults(fn=cmd_status)
143
- sub.add_parser("account", help="show the Claude Max account pool").set_defaults(fn=cmd_account)
144
- sub.add_parser("billing", help="show usage/cost per Claude Max account").set_defaults(fn=cmd_billing)
145
- p_run = sub.add_parser("run", help="run a mission end-to-end")
146
- p_run.add_argument("intent", help="the mission, in natural language")
147
- p_run.set_defaults(fn=cmd_run)
148
- p_proj = sub.add_parser("project", help="create a project")
149
- p_proj.add_argument("name", help="the project name")
150
- p_proj.add_argument("--no-telegram", action="store_true",
151
- help="skip Telegram topic creation")
152
- p_proj.set_defaults(fn=cmd_project)
153
- args = parser.parse_args(argv)
935
+ def cmd_project_secret(args: argparse.Namespace) -> int:
936
+ """Per-project secret CLI: ``omega project secret {set|get|list} <slug> ...``
937
+
938
+ Secrets land under ``Agentik_Coding/projects/<slug>/.secrets/`` and are
939
+ encrypted with the same age recipient as the global vault when age is
940
+ available. Reads succeed only with the global key (so backups can't be
941
+ used to exfiltrate without the key).
942
+ """
943
+ from omega_engine.vault import vault_read, vault_status, vault_write
944
+
945
+ home = _omega_home()
946
+ op = args.op
947
+ slug = args.slug
948
+ if op == "list":
949
+ status = vault_status(home, project=slug)
950
+ sec = Path(status["secrets_dir"])
951
+ if not sec.exists():
952
+ print(f"no secrets yet for project '{slug}'")
953
+ return 0
954
+ print(f"project '{slug}' vault — backend: {status['backend']}")
955
+ print(f" encrypted: {status['encrypted_count']}, "
956
+ f"plaintext: {status['plaintext_count']}, "
957
+ f"placeholders: {status['placeholder_count']}")
958
+ for p in sorted(sec.iterdir()):
959
+ if not p.is_file() or p.name.startswith("."):
960
+ continue
961
+ print(f" - {p.name}")
962
+ return 0
963
+ if op == "set":
964
+ ref = args.ref
965
+ if not ref:
966
+ print("error: --ref required for set")
967
+ return 2
968
+ value = args.value
969
+ if value is None:
970
+ try:
971
+ value = input(f"value for {slug}/{ref}: ")
972
+ except EOFError:
973
+ value = ""
974
+ if not value:
975
+ print("no value provided — aborting")
976
+ return 2
977
+ try:
978
+ path = vault_write(home, ref, value, project=slug)
979
+ except Exception as exc: # noqa: BLE001
980
+ print(f"error: {exc}")
981
+ return 1
982
+ print(f"wrote {path}")
983
+ return 0
984
+ if op == "get":
985
+ ref = args.ref
986
+ if not ref:
987
+ print("error: --ref required for get")
988
+ return 2
989
+ try:
990
+ value = vault_read(home, ref, project=slug)
991
+ except Exception as exc: # noqa: BLE001
992
+ print(f"error: {exc}")
993
+ return 1
994
+ if value is None:
995
+ print(f"no secret '{ref}' for project '{slug}'")
996
+ return 1
997
+ print(value)
998
+ return 0
999
+ print(f"unknown project secret op: {op}")
1000
+ return 2
1001
+
1002
+
1003
+ # --------------------------------------------------------------------------
1004
+ # `omega tool ...` — the registry of third-party tools (incl. installed MCPs).
1005
+ # --------------------------------------------------------------------------
1006
+
1007
+
1008
+ def _tool_install_npm(name: str, package: str, home: Path) -> dict[str, str]:
1009
+ """Install one npm package into Agentik_Tools/<name>/ and return locator info."""
1010
+ import shutil as _sh
1011
+ import subprocess
1012
+
1013
+ tool_dir = home / "Agentik_Tools" / name
1014
+ tool_dir.mkdir(parents=True, exist_ok=True)
1015
+ npm = _sh.which("npm")
1016
+ if npm is None:
1017
+ raise RuntimeError("npm not found on PATH — install Node.js first")
1018
+ subprocess.run(
1019
+ [npm, "install", "-g", "--prefix", str(tool_dir), package],
1020
+ check=True, capture_output=True, text=True, timeout=600,
1021
+ )
1022
+ bin_dir = tool_dir / "bin"
1023
+ invoke = ""
1024
+ if bin_dir.is_dir():
1025
+ bins = sorted(p for p in bin_dir.iterdir() if p.is_file())
1026
+ if bins:
1027
+ invoke = str(bins[0].relative_to(home))
1028
+ return {"path": f"Agentik_Tools/{name}/", "invoke": invoke}
1029
+
1030
+
1031
+ def cmd_tool_list(_args: argparse.Namespace) -> int:
1032
+ from omega_engine.tools import ToolRegistry
1033
+ reg = ToolRegistry.load(_omega_home())
1034
+ tools = reg.list()
1035
+ if not tools:
1036
+ print("(no tools installed)")
1037
+ return 0
1038
+ print(f"{len(tools)} tool(s) installed:")
1039
+ for t in tools:
1040
+ ver = f"@{t.version}" if t.version else ""
1041
+ print(f" {t.name}{ver} source={t.source or '-'} invoke={t.invoke or '-'}")
1042
+ return 0
1043
+
1044
+
1045
+ def cmd_tool_install(args: argparse.Namespace) -> int:
1046
+ """Install a tool — either an MCP id from the catalog OR an npm package."""
1047
+ home = _omega_home()
1048
+ name = args.name
1049
+ from omega_engine.tools import (
1050
+ CatalogError,
1051
+ Tool,
1052
+ ToolRegistry,
1053
+ install_from_catalog,
1054
+ load_catalog,
1055
+ )
1056
+
1057
+ if args.npm is None:
1058
+ # Default path: treat <name> as an MCP catalog id.
1059
+ try:
1060
+ catalog = load_catalog(home)
1061
+ tool = install_from_catalog(home, name, catalog=catalog)
1062
+ print(f"installed {tool.name} (source={tool.source})")
1063
+ return 0
1064
+ except CatalogError as exc:
1065
+ print(f"error: {exc}")
1066
+ return 1
1067
+ except Exception as exc: # noqa: BLE001
1068
+ print(f"error: {exc}")
1069
+ return 1
1070
+
1071
+ # --npm given: install a bare npm package under Agentik_Tools/<name>/.
1072
+ try:
1073
+ loc = _tool_install_npm(name, args.npm, home)
1074
+ except Exception as exc: # noqa: BLE001
1075
+ print(f"error: {exc}")
1076
+ return 1
1077
+ reg = ToolRegistry.load(home)
1078
+ reg.install(Tool(name=name, path=loc["path"], invoke=loc["invoke"],
1079
+ source=f"npm:{args.npm}"))
1080
+ reg.save(home)
1081
+ print(f"installed {name} (npm:{args.npm})")
1082
+ return 0
1083
+
1084
+
1085
+ def cmd_tool_remove(args: argparse.Namespace) -> int:
1086
+ import shutil as _sh
1087
+ from omega_engine.tools import ToolRegistry
1088
+ home = _omega_home()
1089
+ reg = ToolRegistry.load(home)
1090
+ tool = reg.get(args.name)
1091
+ if tool is None:
1092
+ print(f"not installed: {args.name}")
1093
+ return 1
1094
+ tool_dir = home / tool.path
1095
+ if tool_dir.exists():
1096
+ _sh.rmtree(tool_dir, ignore_errors=True)
1097
+ reg.remove(args.name)
1098
+ reg.save(home)
1099
+ print(f"removed {args.name}")
1100
+ return 0
1101
+
1102
+
1103
+ def cmd_tool_update(args: argparse.Namespace) -> int:
1104
+ """Re-install the tool from its recorded source."""
1105
+ from omega_engine.tools import (
1106
+ CatalogError,
1107
+ Tool,
1108
+ ToolRegistry,
1109
+ install_from_catalog,
1110
+ load_catalog,
1111
+ )
1112
+ home = _omega_home()
1113
+ reg = ToolRegistry.load(home)
1114
+ tool = reg.get(args.name)
1115
+ if tool is None:
1116
+ print(f"not installed: {args.name}")
1117
+ return 1
1118
+ if tool.source.startswith("mcp:") or tool.source.startswith("catalog:"):
1119
+ try:
1120
+ catalog = load_catalog(home)
1121
+ install_from_catalog(home, args.name, catalog=catalog, registry=reg)
1122
+ print(f"updated {args.name}")
1123
+ return 0
1124
+ except CatalogError as exc:
1125
+ print(f"error: {exc}")
1126
+ return 1
1127
+ if tool.source.startswith("npm:"):
1128
+ pkg = tool.source.split(":", 1)[1]
1129
+ try:
1130
+ loc = _tool_install_npm(args.name, pkg, home)
1131
+ except Exception as exc: # noqa: BLE001
1132
+ print(f"error: {exc}")
1133
+ return 1
1134
+ reg.install(Tool(name=args.name, path=loc["path"], invoke=loc["invoke"],
1135
+ source=tool.source))
1136
+ reg.save(home)
1137
+ print(f"updated {args.name}")
1138
+ return 0
1139
+ print(f"cannot update — unknown source: {tool.source}")
1140
+ return 1
1141
+
1142
+
1143
+ def cmd_tool(args: argparse.Namespace) -> int:
1144
+ """`omega tool` dispatcher."""
1145
+ sub = getattr(args, "tool_cmd", None) or "list"
1146
+ fns = {
1147
+ "list": cmd_tool_list,
1148
+ "install": cmd_tool_install,
1149
+ "remove": cmd_tool_remove,
1150
+ "update": cmd_tool_update,
1151
+ }
1152
+ if sub not in fns:
1153
+ print(f"unknown tool subcommand: {sub}")
1154
+ return 2
1155
+ return fns[sub](args)
1156
+
1157
+
1158
+ def cmd_sync(_args: argparse.Namespace) -> int:
1159
+ """Project the SSOT into every active provider's native shape."""
1160
+ from omega_engine.sync import SyncEngine
1161
+ outcomes = SyncEngine().sync_all(_omega_home())
1162
+ for o in outcomes:
1163
+ if o.get("projected"):
1164
+ print(
1165
+ f" [ok] {o['provider']}: skills={o['skills_written']} "
1166
+ f"commands={o['commands_written']} "
1167
+ f"agents={o.get('agents_written', 0)} "
1168
+ f"mcp={o['mcp_servers']} hooks={len(o.get('hooks', []))}"
1169
+ )
1170
+ else:
1171
+ print(f" [-] {o['provider']}: {o.get('reason', 'skipped')}")
1172
+ return 0
1173
+
1174
+
1175
+ def cmd_graph(args: argparse.Namespace) -> int:
1176
+ """Consume a graphify-built knowledge graph and (optionally) load it
1177
+ into the engine's Graph RAG.
1178
+
1179
+ Graphify is a **Claude Code skill** — the graph itself is built inside
1180
+ Claude Code by typing ``/graphify .`` in the target project. This
1181
+ command reads the resulting ``<path>/graphify-out/graph.json`` and
1182
+ merges its nodes + edges into the engine's GraphRetriever so the
1183
+ multi-RAG router can answer relational queries about the code.
1184
+
1185
+ Workflow:
1186
+ 1. ``omega graph install`` — register the /graphify skill into ~/.claude/
1187
+ (or wherever your Claude Code skills live).
1188
+ 2. Open Claude Code at <path>, run ``/graphify .`` — it produces
1189
+ ``<path>/graphify-out/graph.json`` + the report + html.
1190
+ 3. ``omega graph <path> --rag`` — merge that graph into the engine's
1191
+ Graph RAG memory.
1192
+ """
1193
+ from omega_engine.integrations import (
1194
+ GraphifyError,
1195
+ GraphifyGraphMissing,
1196
+ GraphifyNotInstalled,
1197
+ graphify_install_skill,
1198
+ graphify_into_rag,
1199
+ graphify_load_graph,
1200
+ graphify_version,
1201
+ is_graphify_installed,
1202
+ )
1203
+
1204
+ # Subcommand: `omega graph install [--platform claude]`
1205
+ if args.path == "install":
1206
+ if not is_graphify_installed():
1207
+ print(
1208
+ "graphify is not installed. Install with:\n"
1209
+ " pip install graphifyy\n"
1210
+ "Then re-run `omega graph install`."
1211
+ )
1212
+ return 1
1213
+ try:
1214
+ result = graphify_install_skill(platform=args.platform)
1215
+ except (GraphifyNotInstalled, GraphifyError) as exc:
1216
+ print(f"graphify install failed: {exc}")
1217
+ return 1
1218
+ print(f"graphify skill installed (platform={result['platform']})")
1219
+ if result.get("stdout"):
1220
+ print(result["stdout"])
1221
+ print("Open Claude Code in any project and type `/graphify .`")
1222
+ return 0
1223
+
1224
+ # Default: load a built graph from <path>/graphify-out/graph.json
1225
+ version = graphify_version() or "(graphify CLI not on PATH — only graph.json needed)"
1226
+ print(f"graphify: {version}")
1227
+
1228
+ try:
1229
+ report = graphify_load_graph(args.path, json_path=args.json)
1230
+ except GraphifyGraphMissing as exc:
1231
+ print(f"{exc}")
1232
+ print(
1233
+ "Open Claude Code at the project and run `/graphify .` to build "
1234
+ "the graph, then re-run `omega graph <path>`."
1235
+ )
1236
+ return 1
1237
+ except GraphifyError as exc:
1238
+ print(f"graphify load failed: {exc}")
1239
+ return 1
1240
+
1241
+ print(f" output: {report.output_dir}")
1242
+ print(f" nodes: {len(report.nodes):,d}")
1243
+ print(f" edges: {len(report.edges):,d}")
1244
+ if report.html_path.exists():
1245
+ print(f" html: {report.html_path}")
1246
+ if report.report_path.exists():
1247
+ print(f" report: {report.report_path}")
1248
+
1249
+ if args.rag:
1250
+ from omega_engine.rag import GraphRetriever
1251
+
1252
+ home = _omega_home()
1253
+ rag_path = home / "Agentik_Runtime" / "memory" / "graph.json"
1254
+ rag_path.parent.mkdir(parents=True, exist_ok=True)
1255
+ retriever = GraphRetriever(json_path=str(rag_path))
1256
+ counts = graphify_into_rag(report, retriever)
1257
+ print(
1258
+ f" rag: merged {counts['nodes']:,d} nodes and "
1259
+ f"{counts['edges']:,d} edges into {rag_path}"
1260
+ )
1261
+ return 0
1262
+
1263
+
1264
+ def cmd_validate(args: argparse.Namespace) -> int:
1265
+ """`omega validate providers` — round-trip every wired provider."""
1266
+ from omega_engine.validate import validate_all, validate_one
1267
+ home = _omega_home()
1268
+ sub = getattr(args, "validate_cmd", None) or "providers"
1269
+ if sub != "providers":
1270
+ print(f"unknown validate subcommand: {sub}")
1271
+ return 2
1272
+ if args.provider:
1273
+ result = validate_one(home, args.provider)
1274
+ results = [result]
1275
+ else:
1276
+ results = validate_all(home)
1277
+ if not results:
1278
+ print("(no providers configured — see manifest providers:)")
1279
+ return 0
1280
+ fail = False
1281
+ for r in results:
1282
+ mark = "ok" if r.ok else "FAIL"
1283
+ print(f" [{mark}] {r.provider:<12} {r.detail[:100]} ({r.elapsed_ms}ms)")
1284
+ if not r.ok:
1285
+ fail = True
1286
+ return 1 if fail else 0
1287
+
1288
+
1289
+ def cmd_completions(args: argparse.Namespace) -> int:
1290
+ """`omega completions` — write + install shell completion scripts."""
1291
+ from omega_engine.completions import (
1292
+ extract_tree, install_for_shell, write_all,
1293
+ )
1294
+ home = _omega_home()
1295
+ sub = args.completions_cmd or "write"
1296
+ if sub == "write":
1297
+ parser = _build_parser()
1298
+ tree = extract_tree(parser)
1299
+ paths = write_all(home, tree)
1300
+ for shell, p in paths.items():
1301
+ print(f" {shell:<5} → {p}")
1302
+ return 0
1303
+ if sub == "install":
1304
+ result = install_for_shell(home, args.shell)
1305
+ if result.get("status") == "ok":
1306
+ print(f" installed {args.shell} completion: {result['link']} → {result['src']}")
1307
+ print(f" source the file in your shell rc or restart your terminal")
1308
+ return 0
1309
+ print(f" error: {result.get('detail', '?')}")
1310
+ return 1
1311
+ print(f"unknown completions subcommand: {sub}")
1312
+ return 2
1313
+
1314
+
1315
+ def cmd_update(args: argparse.Namespace) -> int:
1316
+ """`omega update {check,apply,banner,auto}` — auto-update lifecycle.
1317
+
1318
+ Subcommands:
1319
+ * ``check`` query npm for the latest @agentikos/omega-os (default; legacy)
1320
+ * ``apply`` backup → npm install → migrate → upgrade → verify (rollback on fail)
1321
+ * ``banner`` print the one-line banner if outdated (used by autostart hooks)
1322
+ * ``auto`` enable/disable the daily auto-apply charter
1323
+ """
1324
+ from omega_engine import __version__
1325
+ from omega_engine import auto_update as AU
1326
+ sub = getattr(args, "update_cmd", None)
1327
+ home = _omega_home()
1328
+
1329
+ # Legacy flag — `omega update --check`
1330
+ if sub is None:
1331
+ sub = "check" if getattr(args, "check", False) else None
1332
+ if sub == "apply":
1333
+ force_no_npm = getattr(args, "no_npm", False)
1334
+ res = AU.apply_update(
1335
+ home,
1336
+ skip_backup=getattr(args, "no_backup", False),
1337
+ skip_verify=getattr(args, "no_verify", False),
1338
+ npm_global=not force_no_npm,
1339
+ )
1340
+ import json as _json
1341
+ if getattr(args, "json", False):
1342
+ print(_json.dumps({
1343
+ "ok": res.ok, "from": res.from_version,
1344
+ "to": res.to_version, "rolled_back": res.rolled_back,
1345
+ "bundle": res.bundle, "duration_s": res.duration_s,
1346
+ "error": res.error, "steps": res.steps,
1347
+ }, indent=2))
1348
+ else:
1349
+ mark = "✓" if res.ok else "✗"
1350
+ print(f" {mark} {res.from_version} → {res.to_version} "
1351
+ f"({res.duration_s}s)")
1352
+ if res.bundle:
1353
+ print(f" backup: {res.bundle}")
1354
+ if res.rolled_back:
1355
+ print(f" ROLLED BACK — restored from backup")
1356
+ if res.error:
1357
+ print(f" {res.error}")
1358
+ for st in res.steps:
1359
+ rc = st.get("rc", "?")
1360
+ tag = "ok" if rc == 0 else f"FAIL({rc})"
1361
+ print(f" [{tag:8s}] {st.get('label')}")
1362
+ return 0 if res.ok else 1
1363
+
1364
+ if sub == "banner":
1365
+ # Called by shell autostart; prints to stderr, never blocks.
1366
+ printed = AU.maybe_emit_banner(home)
1367
+ return 0
1368
+
1369
+ if sub == "auto":
1370
+ # Manage the auto-apply charter (writes to Agentik_Orchestration/autonomous/).
1371
+ op = getattr(args, "auto_op", "status")
1372
+ charters = (
1373
+ home / "Agentik_Orchestration" / "autonomous"
1374
+ )
1375
+ charter = charters / "auto-update.yaml"
1376
+ if op == "enable":
1377
+ charters.mkdir(parents=True, exist_ok=True)
1378
+ charter.write_text(
1379
+ "# Daily auto-apply of @agentikos/omega-os updates.\n"
1380
+ "id: auto-update\n"
1381
+ "description: Check npm + apply any available update at 04:00 UTC.\n"
1382
+ "enabled: true\n"
1383
+ "trigger:\n"
1384
+ " type: cron\n"
1385
+ " config:\n"
1386
+ " cron: \"0 4 * * *\"\n"
1387
+ "intent: |\n"
1388
+ " Run `omega update apply --json` and report the result.\n"
1389
+ " If rolled_back=true, send the operator a Telegram alert.\n"
1390
+ )
1391
+ print(f"auto-update charter enabled at {charter}")
1392
+ return 0
1393
+ if op == "disable":
1394
+ if charter.exists():
1395
+ charter.unlink()
1396
+ print("auto-update charter removed")
1397
+ else:
1398
+ print("auto-update was not enabled")
1399
+ return 0
1400
+ # status
1401
+ if charter.exists():
1402
+ print(f"auto-update: ENABLED ({charter})")
1403
+ else:
1404
+ print("auto-update: disabled (use `omega update auto enable`)")
1405
+ return 0
1406
+
1407
+ # `omega update check` (default) — same legacy behaviour.
1408
+ from omega_engine.updater import check_for_updates
1409
+ info = check_for_updates(__version__)
1410
+ if info.error:
1411
+ print(f" (update check failed: {info.error})")
1412
+ return 1
1413
+ if info.is_outdated:
1414
+ print(f" current: {info.current}")
1415
+ print(f" latest: {info.latest} (update available)")
1416
+ print(f" run: omega update apply")
1417
+ return 0
1418
+ print(f" up to date — current {info.current} == latest {info.latest}")
1419
+ return 0
1420
+
1421
+
1422
+ def cmd_smoke(args: argparse.Namespace) -> int:
1423
+ """`omega smoke` — synthetic mission to verify the orchestration stack."""
1424
+ from omega_engine.smoke import run_smoke
1425
+ result = run_smoke()
1426
+ mark = "ok" if result.passed else "FAIL"
1427
+ print(f" [{mark}] smoke ({result.elapsed_ms}ms): {result.detail}")
1428
+ if result.mission_id:
1429
+ print(f" mission_id={result.mission_id} "
1430
+ f"final_state={result.final_state} "
1431
+ f"events={result.events_recorded} "
1432
+ f"done_files={result.done_files_written}")
1433
+ return 0 if result.passed else 1
1434
+
1435
+
1436
+ def cmd_prune(args: argparse.Namespace) -> int:
1437
+ """`omega prune` — bound the SQLite stores' growth."""
1438
+ from omega_engine import prune as P
1439
+ home = _omega_home()
1440
+ target = args.target
1441
+ days = args.days
1442
+ dry = not args.yes
1443
+ if target == "all":
1444
+ reports = P.prune_all(home, older_than_days=days, dry_run=dry)
1445
+ elif target == "events":
1446
+ reports = [P.prune_events(home, older_than_days=days, dry_run=dry)]
1447
+ elif target == "audits":
1448
+ reports = [P.prune_audits(home, older_than_days=days, dry_run=dry)]
1449
+ elif target == "telegram":
1450
+ reports = [P.prune_telegram(home, older_than_days=days, dry_run=dry)]
1451
+ elif target == "sessions":
1452
+ reports = [P.prune_sessions(home, older_than_days=days, dry_run=dry)]
1453
+ else:
1454
+ print(f"unknown target: {target}")
1455
+ return 2
1456
+ for r in reports:
1457
+ if r.target == "sessions":
1458
+ verb = "would remove" if r.dry_run else "removed"
1459
+ print(f" {r.target:<10} {verb} {r.files_deleted} session(s), "
1460
+ f"{r.bytes_freed:,d} bytes ({r.detail})")
1461
+ else:
1462
+ verb = "would delete" if r.dry_run else "deleted"
1463
+ print(f" {r.target:<10} {verb} {r.rows_deleted} row(s) ({r.detail})")
1464
+ if dry:
1465
+ print(" (dry-run — re-run with --yes to apply)")
1466
+ return 0
1467
+
1468
+
1469
+ def cmd_backup(args: argparse.Namespace) -> int:
1470
+ """`omega backup` — tarball of the recoverable subset, optionally age-encrypted."""
1471
+ from omega_engine.backup import backup, restore, verify
1472
+ home = _omega_home()
1473
+ sub = args.backup_cmd
1474
+ if sub == "create" or sub is None:
1475
+ try:
1476
+ result = backup(
1477
+ home, out_dir=args.out, encrypt=args.encrypt,
1478
+ recipient_path=args.recipient,
1479
+ )
1480
+ except Exception as exc: # noqa: BLE001
1481
+ print(f" error: {exc}")
1482
+ return 1
1483
+ print(f" bundle: {result.bundle}")
1484
+ print(f" size: {result.bytes:,d} bytes encrypted={result.encrypted}")
1485
+ print(f" duration: {result.duration_s}s")
1486
+ return 0
1487
+ if sub == "restore":
1488
+ if not args.bundle:
1489
+ print("usage: omega backup restore <bundle> [--target DIR] [--identity FILE]")
1490
+ return 2
1491
+ try:
1492
+ result = restore(
1493
+ args.bundle,
1494
+ target=(args.target or home),
1495
+ age_identity=args.identity,
1496
+ )
1497
+ except Exception as exc: # noqa: BLE001
1498
+ print(f" error: {exc}")
1499
+ return 1
1500
+ print(f" restored {result.files} file(s) into {result.target}")
1501
+ return 0
1502
+ if sub == "verify":
1503
+ if not args.bundle:
1504
+ print("usage: omega backup verify <bundle> [--identity FILE]")
1505
+ return 2
1506
+ try:
1507
+ result = verify(args.bundle, age_identity=args.identity)
1508
+ except Exception as exc: # noqa: BLE001
1509
+ print(f" error: {exc}")
1510
+ return 1
1511
+ mark = "ok" if result["ok"] else "FAIL"
1512
+ print(f" [{mark}] {result['detail']} ({result['files']} files)")
1513
+ return 0 if result["ok"] else 1
1514
+ print(f"unknown backup subcommand: {sub}")
1515
+ return 2
1516
+
1517
+
1518
+ def cmd_costs(args: argparse.Namespace) -> int:
1519
+ """`omega costs` — project usage into USD per provider/account."""
1520
+ from omega_engine.account import AccountPool, BillingAggregator
1521
+ from omega_engine.costs import (
1522
+ RATE_CARD_DATE, estimate_cost, rate_card_summary,
1523
+ )
1524
+ from omega_engine.store import SQLiteStore
1525
+ home = _omega_home()
1526
+ print(f"omega costs — rate card from {RATE_CARD_DATE}")
1527
+ print(f" providers covered: {', '.join(rate_card_summary()['providers'])}")
1528
+ print()
1529
+ store_path = home / "Agentik_Runtime" / "eventlog" / "omega.db"
1530
+ if not store_path.exists():
1531
+ print(" (no event store yet)")
1532
+ return 0
1533
+ store = SQLiteStore(store_path)
1534
+ totals = BillingAggregator.from_event_log(store)
1535
+ pool = AccountPool.load(home)
1536
+ if not totals:
1537
+ print(" (no per-account usage recorded)")
1538
+ store.close()
1539
+ return 0
1540
+ pool_ids = {a.id: a for a in pool.accounts}
1541
+ grand = 0.0
1542
+ print(f" {'account':<18} {'provider':<10} {'in_tok':>12} {'out_tok':>12} {'USD':>8}")
1543
+ print(f" {'-' * 64}")
1544
+ for acc_id, t in sorted(totals.items()):
1545
+ # Assume Claude unless the operator overrode in accounts.yaml later.
1546
+ provider = "claude"
1547
+ cost = estimate_cost(
1548
+ provider,
1549
+ input_tokens=int(t["input_tokens"]),
1550
+ output_tokens=int(t["output_tokens"]),
1551
+ )
1552
+ grand += cost
1553
+ print(f" {acc_id:<18} {provider:<10} {t['input_tokens']:>12,d} "
1554
+ f"{t['output_tokens']:>12,d} {cost:>7.2f}")
1555
+ print(f" {'-' * 64}")
1556
+ print(f" {'TOTAL':<29} {grand:>40.2f} USD")
1557
+ store.close()
1558
+ return 0
1559
+
1560
+
1561
+ def cmd_audit_diff(args: argparse.Namespace) -> int:
1562
+ """`omega audit diff <run-id-1> <run-id-2>` — structural diff of two runs."""
1563
+ from omega_engine.audit_diff import diff_runs
1564
+ home = _omega_home()
1565
+ try:
1566
+ d = diff_runs(home, args.run1, args.run2)
1567
+ except KeyError as exc:
1568
+ print(f"error: {exc}")
1569
+ return 2
1570
+ print(f"audit diff: {d.audit_id}")
1571
+ print(f" {d.run1} → {d.run2}")
1572
+ print(f" score: {d.score1} → {d.score2} (delta {d.delta:+d})")
1573
+ print(f" fixed: {len(d.fixed)}")
1574
+ print(f" new: {len(d.new)}")
1575
+ print(f" persist: {len(d.persist)}")
1576
+ print(f" regressed: {len(d.regressed)}")
1577
+ if d.new:
1578
+ print(f" NEW findings:")
1579
+ for f in d.new[:5]:
1580
+ print(f" [{f.get('severity', '?')}] {f.get('location', '?')} "
1581
+ f"— {str(f.get('message', f.get('claim', '')))[:60]}")
1582
+ if d.regressed:
1583
+ print(f" REGRESSED:")
1584
+ for f in d.regressed[:5]:
1585
+ print(f" [{f.get('severity', '?')}] {f.get('location', '?')}")
1586
+ return 0
1587
+
1588
+
1589
+ def cmd_mission(args: argparse.Namespace) -> int:
1590
+ """`omega mission rerun <id>` — re-dispatch a past mission with fresh state."""
1591
+ from omega_engine.reducer import reduce_task
1592
+ from omega_engine.store import SQLiteStore
1593
+ home = _omega_home()
1594
+ sub = args.mission_cmd
1595
+ if sub == "rerun":
1596
+ store_path = home / "Agentik_Runtime" / "eventlog" / "omega.db"
1597
+ if not store_path.exists():
1598
+ print("no event store yet")
1599
+ return 1
1600
+ store = SQLiteStore(store_path)
1601
+ # Find the root task for this mission_id by scanning created events.
1602
+ events = store.all_events() if hasattr(store, "all_events") else []
1603
+ if not events:
1604
+ # Fall back: load events for each task and find one with matching mission_id.
1605
+ for tid in store.task_ids():
1606
+ for evt in store.events_for(tid):
1607
+ if (evt.payload.get("mission_id") == args.id
1608
+ and evt.payload.get("role") == "oracle"):
1609
+ events.append(evt)
1610
+ break
1611
+ # Extract intent from the first created event with that mission_id.
1612
+ intent = None
1613
+ for tid in store.task_ids():
1614
+ for evt in store.events_for(tid):
1615
+ if (evt.payload.get("mission_id") == args.id
1616
+ and evt.type.value == "task.created"
1617
+ and evt.payload.get("role") == "oracle"):
1618
+ # The original intent is in the spec; we don't have it directly
1619
+ # on the event. Best-effort: use the description from the
1620
+ # store if we can find it. Otherwise fall back to a hint.
1621
+ spec = evt.payload.get("spec") or {}
1622
+ intent = spec.get("intent") or spec.get("task")
1623
+ break
1624
+ if intent:
1625
+ break
1626
+ store.close()
1627
+ if not intent:
1628
+ print(f"could not find original intent for mission {args.id}")
1629
+ return 1
1630
+ print(f"re-running mission with intent: {intent[:100]}")
1631
+ from omega_engine.mission import run_mission
1632
+ outcome = run_mission(intent)
1633
+ print(f"new mission: {outcome.result.mission_id} "
1634
+ f"final={outcome.result.final_state.value}")
1635
+ return 0 if outcome.result.verified else 1
1636
+ print(f"unknown mission subcommand: {sub}")
1637
+ return 2
1638
+
1639
+
1640
+ def cmd_precommit(args: argparse.Namespace) -> int:
1641
+ """`omega project precommit install <slug>` — wire audit into pre-commit."""
1642
+ import shutil as _sh
1643
+ home = _omega_home()
1644
+ sub = args.precommit_cmd or "install"
1645
+ proj_dir = home / "Agentik_Coding" / "projects" / args.slug
1646
+ if not proj_dir.is_dir():
1647
+ print(f"project not found: {proj_dir}")
1648
+ return 1
1649
+ if sub == "install":
1650
+ hooks_dir = proj_dir / ".git" / "hooks"
1651
+ if not hooks_dir.is_dir():
1652
+ print(f" not a git repo: {proj_dir}/.git missing")
1653
+ return 1
1654
+ hook = hooks_dir / "pre-commit"
1655
+ if hook.exists():
1656
+ backup = hook.with_suffix(".pre-omega.bak")
1657
+ _sh.copy(hook, backup)
1658
+ print(f" backed up existing hook → {backup}")
1659
+ omega_bin = home / "Agentik_Tools" / "bin" / "omega"
1660
+ hook.write_text(f"""#!/usr/bin/env bash
1661
+ # omega pre-commit — audit before code lands.
1662
+ set -e
1663
+ "{omega_bin}" audit run codeaudit --scope . || {{
1664
+ echo "omega: codeaudit failed — commit blocked. Run: omega audit run codeaudit --fix"
1665
+ exit 1
1666
+ }}
1667
+ """)
1668
+ hook.chmod(0o755)
1669
+ print(f" pre-commit hook installed at {hook}")
1670
+ return 0
1671
+ if sub == "uninstall":
1672
+ hook = proj_dir / ".git" / "hooks" / "pre-commit"
1673
+ if hook.exists():
1674
+ hook.unlink()
1675
+ print(f" removed {hook}")
1676
+ backup = hook.with_suffix(".pre-omega.bak")
1677
+ if backup.exists():
1678
+ _sh.move(backup, hook)
1679
+ print(f" restored prior hook from {backup}")
1680
+ return 0
1681
+ print(f"unknown precommit subcommand: {sub}")
1682
+ return 2
1683
+
1684
+
1685
+ def cmd_webhook(args: argparse.Namespace) -> int:
1686
+ """Inbound-webhook CLI: list, show, test.
1687
+
1688
+ Webhooks live in ``Agentik_SSOT/webhooks/webhooks.yaml``. The engine
1689
+ daemon's HTTP server listens at ``127.0.0.1:OMEGA_ENGINE_PORT/webhook/<id>``
1690
+ — pair with a reverse proxy / tunnel for the public face. The auth
1691
+ is HMAC-SHA256 signature verification per source (GitHub / Linear /
1692
+ generic).
1693
+ """
1694
+ from omega_engine.webhooks import (
1695
+ WebhookSpec,
1696
+ generate_test_signature,
1697
+ handle_webhook,
1698
+ load_registry,
1699
+ )
1700
+ home = _omega_home()
1701
+ sub = args.webhook_cmd
1702
+ reg = load_registry(home)
1703
+
1704
+ if sub is None or sub == "list":
1705
+ if not reg.specs:
1706
+ print("(no webhooks configured — see manifest.example.yaml `webhooks:` block)")
1707
+ return 0
1708
+ for s in reg.specs:
1709
+ print(f" [{s.source:<7}] {s.id:<20} {s.path} (secret_ref={s.secret_ref})")
1710
+ if s.description:
1711
+ print(f" {s.description}")
1712
+ return 0
1713
+
1714
+ if sub == "show":
1715
+ s = reg.by_id(args.id)
1716
+ if s is None:
1717
+ print(f"no webhook with id {args.id!r}")
1718
+ return 1
1719
+ import json as _json
1720
+ print(_json.dumps(s.__dict__, indent=2))
1721
+ return 0
1722
+
1723
+ if sub == "test":
1724
+ s = reg.by_id(args.id)
1725
+ if s is None:
1726
+ print(f"no webhook with id {args.id!r}")
1727
+ return 1
1728
+ # Load the secret + build a signed payload + run the gate locally.
1729
+ from omega_engine.webhooks import _load_secret
1730
+ secret = _load_secret(home, s.secret_ref)
1731
+ if not secret:
1732
+ print(
1733
+ f"error: secret {s.secret_ref!r} is not in the vault — "
1734
+ f"set it with `omega project secret set` or write to the vault first"
1735
+ )
1736
+ return 2
1737
+ body = (args.body or '{"intent": "synthetic test"}').encode("utf-8")
1738
+ sig = generate_test_signature(s.source, body, secret)
1739
+ from omega_engine.webhooks import signature_header_for
1740
+ headers = {signature_header_for(s.source): sig}
1741
+ result = handle_webhook(
1742
+ home, path=s.path, headers=headers, body=body, supervisor=None,
1743
+ )
1744
+ print(f" gate: status={result['status']} ok={result['ok']} "
1745
+ f"reason={result['reason']}")
1746
+ return 0 if result["ok"] else 1
1747
+
1748
+ print(f"unknown webhook subcommand: {sub}")
1749
+ return 2
1750
+
1751
+
1752
+ def cmd_uninstall(args: argparse.Namespace) -> int:
1753
+ """Clean removal of OmegaOS from this machine.
1754
+
1755
+ What we touch (with confirmations):
1756
+
1757
+ * the 8-block tree at ``$OMEGA_HOME`` (the SSOT + engine + runtime)
1758
+ * the symlink ``Agentik_Tools/bin/omega``
1759
+ * the OmegaOS-specific keys in ``~/.claude/settings.json``
1760
+ (env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS, the Stop-hook entry
1761
+ tagged ``omega-audit-gate``)
1762
+ * the systemd user units / launchd plists
1763
+ * optionally the LLM CLIs (``--include-llm-clis``)
1764
+
1765
+ What we DON'T touch unless asked:
1766
+
1767
+ * the encrypted vault — without ``--purge``, we move it to
1768
+ ``$HOME/.omega-vault-backup-<timestamp>/`` so the operator can
1769
+ decrypt later
1770
+ * the LLM CLIs (need ``--include-llm-clis``)
1771
+ """
1772
+ import json as _json
1773
+ import shutil as _sh
1774
+ import subprocess
1775
+ import time
1776
+
1777
+ home = _omega_home()
1778
+ print(f"omega uninstall — OMEGA_HOME={home}")
1779
+ if not home.exists():
1780
+ print(" (nothing to remove — directory does not exist)")
1781
+ return 0
1782
+
1783
+ if not args.yes:
1784
+ try:
1785
+ resp = input(f" remove {home} and related files? [y/N] ").strip()
1786
+ except EOFError:
1787
+ resp = ""
1788
+ if resp.lower() not in {"y", "yes"}:
1789
+ print(" aborted")
1790
+ return 1
1791
+
1792
+ # 1. Stop + disable services first (best effort).
1793
+ import platform
1794
+ if platform.system() == "Linux" and _sh.which("systemctl"):
1795
+ for unit in ("omega-engine", "omega-telegram", "omega-autonomous"):
1796
+ subprocess.run(
1797
+ ["systemctl", "--user", "disable", "--now", f"{unit}.service"],
1798
+ check=False, capture_output=True, timeout=30,
1799
+ )
1800
+ unitdir = Path.home() / ".config" / "systemd" / "user"
1801
+ for unit in ("omega-engine", "omega-telegram", "omega-autonomous"):
1802
+ (unitdir / f"{unit}.service").unlink(missing_ok=True)
1803
+ subprocess.run(["systemctl", "--user", "daemon-reload"],
1804
+ check=False, capture_output=True, timeout=15)
1805
+ print(" removed systemd user services")
1806
+ elif platform.system() == "Darwin" and _sh.which("launchctl"):
1807
+ agentdir = Path.home() / "Library" / "LaunchAgents"
1808
+ for label in ("com.agentikos.omega.engine",
1809
+ "com.agentikos.omega.telegram",
1810
+ "com.agentikos.omega.autonomous"):
1811
+ p = agentdir / f"{label}.plist"
1812
+ if p.exists():
1813
+ subprocess.run(["launchctl", "bootout", f"gui/{os.getuid()}",
1814
+ str(p)], check=False, capture_output=True,
1815
+ timeout=15)
1816
+ p.unlink(missing_ok=True)
1817
+ print(" removed launchd LaunchAgent plists")
1818
+
1819
+ # 2. Settings.json — strip our keys, keep operator changes.
1820
+ settings_path = Path.home() / ".claude" / "settings.json"
1821
+ if settings_path.exists():
1822
+ try:
1823
+ data = _json.loads(settings_path.read_text())
1824
+ changed = False
1825
+ env_block = data.get("env") or {}
1826
+ if "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS" in env_block:
1827
+ env_block.pop("CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS")
1828
+ data["env"] = env_block
1829
+ changed = True
1830
+ hooks = data.get("hooks") or {}
1831
+ stop_entries = hooks.get("Stop") or []
1832
+ cleaned = [
1833
+ e for e in stop_entries
1834
+ if not (isinstance(e, dict) and e.get("tag") == "omega-audit-gate")
1835
+ ]
1836
+ if len(cleaned) != len(stop_entries):
1837
+ hooks["Stop"] = cleaned
1838
+ data["hooks"] = hooks
1839
+ changed = True
1840
+ if changed:
1841
+ settings_path.write_text(_json.dumps(data, indent=2) + "\n")
1842
+ print(f" cleaned OmegaOS keys from {settings_path}")
1843
+ except Exception as exc: # noqa: BLE001
1844
+ print(f" (could not edit settings.json: {exc})")
1845
+
1846
+ # 3. Vault — back up unless --purge.
1847
+ secrets = home / "Agentik_Extra" / "etc" / "secrets"
1848
+ if secrets.is_dir():
1849
+ if args.purge:
1850
+ print(f" purging vault at {secrets}")
1851
+ else:
1852
+ backup = (Path.home() /
1853
+ f".omega-vault-backup-{int(time.time())}")
1854
+ _sh.move(str(secrets), str(backup))
1855
+ print(f" vault backed up to {backup} (move back if you reinstall)")
1856
+
1857
+ # 4. Remove the 8-block tree.
1858
+ print(f" removing {home}")
1859
+ try:
1860
+ _sh.rmtree(home)
1861
+ except OSError as exc:
1862
+ print(f" WARNING: removal incomplete: {exc}")
1863
+ return 1
1864
+
1865
+ # 5. Optional: LLM CLIs.
1866
+ if args.include_llm_clis:
1867
+ helper = (Path(os.environ.get("OMEGA_REPO", ".")) /
1868
+ "bootstrap" / "lib" / "llm-clis.py")
1869
+ if helper.exists():
1870
+ print(" uninstalling LLM CLIs...")
1871
+ subprocess.run(
1872
+ ["python3", str(helper), "uninstall",
1873
+ "claude_code", "gemini_cli", "codex", "glm_sdk", "aider"],
1874
+ check=False, timeout=600,
1875
+ )
1876
+ else:
1877
+ print(" (llm-clis helper unavailable — skipped)")
1878
+
1879
+ print("uninstall complete")
1880
+ return 0
1881
+
1882
+
1883
+ def cmd_upgrade(args: argparse.Namespace) -> int:
1884
+ """Smart re-install: detect existing tree, run install.sh in-place.
1885
+
1886
+ Idempotent. Use to pick up new install steps without losing the
1887
+ runtime tree (event log, vault, account pool). Equivalent to running
1888
+ ``bash $OMEGA_REPO/install.sh --force`` from inside.
1889
+ """
1890
+ import subprocess
1891
+ repo = os.environ.get("OMEGA_REPO")
1892
+ if not repo:
1893
+ # Try to infer from the engine's location.
1894
+ engine_path = Path(__file__).resolve().parent.parent.parent
1895
+ candidate = engine_path.parent
1896
+ if (candidate / "install.sh").exists():
1897
+ repo = str(candidate.parent)
1898
+ else:
1899
+ print("error: OMEGA_REPO is not set and install.sh cannot be located")
1900
+ print(" set OMEGA_REPO=<path-to-OmegaOS-clone> and retry")
1901
+ return 1
1902
+ install_sh = Path(repo) / "install.sh"
1903
+ if not install_sh.exists():
1904
+ print(f"error: install.sh not found at {install_sh}")
1905
+ return 1
1906
+ cmd = ["bash", str(install_sh), "--force"]
1907
+ if args.profile:
1908
+ cmd += ["--profile", args.profile]
1909
+ if args.manifest:
1910
+ cmd += ["--manifest", args.manifest]
1911
+ if args.non_interactive:
1912
+ cmd += ["--non-interactive"]
1913
+ print(f"upgrading via {' '.join(cmd)}")
1914
+ # 30 minutes — install steps can be slow (apt-get + npm + venv build).
1915
+ proc = subprocess.run(cmd, check=False, timeout=1800)
1916
+ return proc.returncode
1917
+
1918
+
1919
+ def cmd_skill(args: argparse.Namespace) -> int:
1920
+ """Skill discovery + safety audit CLI.
1921
+
1922
+ Subcommands:
1923
+ * ``find [<query>]`` — list curated catalog entries
1924
+ * ``audit <path>`` — run static safety auditor on a SKILL.md
1925
+ * ``install <name> --from github:<org>/<repo>[/<path>]``
1926
+ — git-clone + audit + place in SSOT
1927
+ * ``marketplaces`` — show the catalog with trust levels
1928
+ """
1929
+ sub = args.skill_cmd
1930
+ home = _omega_home()
1931
+ if sub is None:
1932
+ print("usage: omega skill {find|audit|install|marketplaces} ...")
1933
+ return 2
1934
+
1935
+ if sub == "marketplaces":
1936
+ from omega_engine.skill_discovery import load_marketplaces
1937
+ for m in load_marketplaces(home):
1938
+ mark = " (builtin)" if m.builtin else ""
1939
+ print(f" [{m.trust:<6}] {m.id:<25} {m.source}{mark}")
1940
+ if m.description:
1941
+ print(f" {m.description}")
1942
+ return 0
1943
+
1944
+ if sub == "find":
1945
+ from omega_engine.skill_discovery import discover_skills
1946
+ hits = discover_skills(
1947
+ home, query=args.query, min_trust=args.min_trust,
1948
+ recommended_only=args.recommended,
1949
+ )
1950
+ if not hits:
1951
+ print("(no skills match)")
1952
+ return 0
1953
+ for s in hits:
1954
+ rec = " ★" if s.recommended else " "
1955
+ print(
1956
+ f"{rec}[{s.marketplace_trust:<6}] {s.name:<22} "
1957
+ f"({s.marketplace_id})"
1958
+ )
1959
+ if s.description:
1960
+ print(f" {s.description}")
1961
+ return 0
1962
+
1963
+ if sub == "audit":
1964
+ from omega_engine.skill_discovery import audit_skill_file
1965
+ v = audit_skill_file(args.path)
1966
+ print(f"audit: {v.skill_name} score={v.score} "
1967
+ f"verified={v.verified}")
1968
+ if v.issues:
1969
+ for i in v.issues:
1970
+ print(f" [{i.severity:<5}] {i.rule}: {i.message}")
1971
+ if i.excerpt:
1972
+ print(f" excerpt: {i.excerpt}")
1973
+ return 0 if v.verified else 1
1974
+
1975
+ if sub == "recommend":
1976
+ from omega_engine.skill_routing import (
1977
+ POWER_SKILLS, catalog_for_home, recommend,
1978
+ )
1979
+ role = args.role
1980
+ recs = recommend(role)
1981
+ catalog = set(catalog_for_home(home))
1982
+ if not recs:
1983
+ print(f"no curated recommendation for role '{role}'")
1984
+ print(f"power skills: {', '.join(POWER_SKILLS)}")
1985
+ return 0
1986
+ print(f"## Recommended skills for role '{role}'")
1987
+ for s in recs:
1988
+ present = "✓" if s in catalog else "✗"
1989
+ print(f" {present} {s}")
1990
+ missing = [s for s in recs if s not in catalog]
1991
+ if missing:
1992
+ print()
1993
+ print(
1994
+ f"missing from SSOT: {', '.join(missing)} "
1995
+ "(run `omega sync` to project, or `omega skill install …`)"
1996
+ )
1997
+ return 0
1998
+
1999
+ if sub == "catalog":
2000
+ from omega_engine.skill_routing import catalog_for_home
2001
+ cat = catalog_for_home(home)
2002
+ if not cat:
2003
+ print(f"no skills under {home}/Agentik_SSOT/skills/")
2004
+ return 0
2005
+ print(f"## {len(cat)} skills in $OMEGA_HOME/Agentik_SSOT/skills/")
2006
+ for s in cat:
2007
+ print(f" /{s}")
2008
+ return 0
2009
+
2010
+ if sub == "link":
2011
+ # Bridge the projected SSOT skills into ~/.claude/skills/ so they
2012
+ # become real slash commands in Claude Code. Idempotent.
2013
+ from omega_engine.sync import ClaudeCodeAdapter
2014
+ adapter = ClaudeCodeAdapter()
2015
+ # Make sure the provider tree exists first.
2016
+ adapter.project(home)
2017
+ info = adapter.link_to_user_claude(home)
2018
+ print(f"linked → {info['user_claude']}")
2019
+ print(f" skills: +{info['linked_skills']}")
2020
+ print(f" commands: +{info['linked_commands']}")
2021
+ print(f" agents: +{info['linked_agents']}")
2022
+ if info["removed"]:
2023
+ print(f" removed stale links: {len(info['removed'])}")
2024
+ return 0
2025
+
2026
+ if sub == "workflows":
2027
+ # Show canonical workflow chains so operators can SEE how skills
2028
+ # compose. This is documentation surfaced via the CLI — no I/O.
2029
+ from omega_engine.skill_routing import WORKFLOWS
2030
+ if args.name:
2031
+ wf = WORKFLOWS.get(args.name)
2032
+ if not wf:
2033
+ print(f"unknown workflow: {args.name}")
2034
+ print(f"available: {', '.join(WORKFLOWS.keys())}")
2035
+ return 2
2036
+ print(f"## {args.name}")
2037
+ print()
2038
+ print(wf)
2039
+ return 0
2040
+ print("## Canonical skill workflows")
2041
+ print()
2042
+ for name, body in WORKFLOWS.items():
2043
+ first_line = body.strip().splitlines()[0]
2044
+ print(f" {name:24s} {first_line}")
2045
+ print()
2046
+ print("Run `omega skill workflows <name>` for the full chain.")
2047
+ return 0
2048
+
2049
+ if sub == "install":
2050
+ if not args.from_:
2051
+ print("usage: omega skill install <name> --from github:<org>/<repo>[/<path>]")
2052
+ return 2
2053
+ if not args.from_.startswith("github:"):
2054
+ print("only github:<org>/<repo>[/<path>] sources are supported "
2055
+ "in CLI install; use `claude plugin install` for marketplace plugins")
2056
+ return 2
2057
+ repo_and_path = args.from_.split(":", 1)[1]
2058
+ if "/" in repo_and_path:
2059
+ parts = repo_and_path.split("/")
2060
+ if len(parts) >= 3:
2061
+ repo = "/".join(parts[:2])
2062
+ sub_path = "/".join(parts[2:])
2063
+ else:
2064
+ repo = repo_and_path; sub_path = ""
2065
+ else:
2066
+ repo = repo_and_path; sub_path = ""
2067
+ from omega_engine.skill_discovery import install_skill_from_github
2068
+ result = install_skill_from_github(
2069
+ home, args.name, repo=repo, sub_path=sub_path,
2070
+ allow_blocked=args.allow_blocked,
2071
+ )
2072
+ if result.audit:
2073
+ print(f"audit: score={result.audit.score} verified={result.audit.verified}")
2074
+ for i in result.audit.issues:
2075
+ print(f" [{i.severity:<5}] {i.rule}: {i.message}")
2076
+ if result.installed:
2077
+ print(f"installed: {result.skill_path}")
2078
+ print("run `omega sync` to project into ~/.claude/skills/")
2079
+ return 0
2080
+ print(f"NOT installed — {result.reason}")
2081
+ return 1
2082
+
2083
+ print(f"unknown skill subcommand: {sub}")
2084
+ return 2
2085
+
2086
+
2087
+ def cmd_agent(args: argparse.Namespace) -> int:
2088
+ """Inspect or list the resolved agent prompts.
2089
+
2090
+ ``omega agent list`` → every loadable role under
2091
+ Agentik_SSOT/agents/.
2092
+ ``omega agent prompt <role>`` → print the system prompt that the
2093
+ envelope builds for that role
2094
+ (agent file + LMC protocol +
2095
+ orchestration contract).
2096
+ """
2097
+ sub = args.agent_cmd
2098
+ home = _omega_home()
2099
+ if sub is None or sub == "list":
2100
+ from omega_engine.prompts import list_available_agents
2101
+ suites = list_available_agents(home)
2102
+ if not suites:
2103
+ print(f"(no agents in {home}/Agentik_SSOT/agents/)")
2104
+ return 0
2105
+ for suite, roles in suites.items():
2106
+ print(f"suite '{suite}': {len(roles)} agent(s)")
2107
+ for r in roles:
2108
+ print(f" - {r}")
2109
+ return 0
2110
+ if sub == "prompt":
2111
+ from omega_engine.envelope import EnvelopeContext, build_envelope
2112
+ ctx = EnvelopeContext(
2113
+ role=args.role,
2114
+ intent=args.intent or "(no intent — preview only)",
2115
+ task_id="preview",
2116
+ )
2117
+ env = build_envelope(home, ctx)
2118
+ print("=== SYSTEM ===")
2119
+ print(env["system"])
2120
+ print()
2121
+ print("=== USER ===")
2122
+ print(env["user"])
2123
+ return 0
2124
+ print(f"unknown agent subcommand: {sub}")
2125
+ return 2
2126
+
2127
+
2128
+ def cmd_audit(args: argparse.Namespace) -> int:
2129
+ """The unified audit CLI.
2130
+
2131
+ Subcommands:
2132
+ * ``run <id>`` — run one audit (gather + analyse).
2133
+ * ``run <id> --fix`` — also batch + dispatch fix workers + re-audit.
2134
+ * ``all`` — run every applicable audit (read-only).
2135
+ * ``history <id>`` — show past scores for one audit.
2136
+ * ``batch <id>`` — show what batches the last run would form
2137
+ (dry-run; no dispatch).
2138
+ * ``gen`` — regenerate every SKILL.md from the YAMLs.
2139
+ """
2140
+ sub = args.audit_cmd
2141
+ home = _omega_home()
2142
+ if sub is None:
2143
+ print("usage: omega audit {run|all|history|batch|gen} ...")
2144
+ return 2
2145
+
2146
+ if sub == "gen":
2147
+ from omega_engine.audits.generator import write_all
2148
+ result = write_all(home / "Agentik_SSOT" / "audits")
2149
+ print(f"audit SKILL.md generation: "
2150
+ f"{result['written']} written, {result['skipped']} unchanged")
2151
+ return 0
2152
+
2153
+ if sub == "gate":
2154
+ # Called by the Claude Code Stop hook — reads JSON on stdin and
2155
+ # emits the permission decision on stdout. See omega_engine.audit_gate.
2156
+ from omega_engine.audit_gate import gate
2157
+ return gate()
2158
+
2159
+ if sub == "history":
2160
+ from omega_engine.audits.history import list_runs, trend
2161
+ runs = list_runs(home, audit_id=args.audit_id, limit=args.limit)
2162
+ if not runs:
2163
+ print(f"no runs recorded for {args.audit_id!r}")
2164
+ return 0
2165
+ symbol = trend(home, args.audit_id) if args.audit_id else "→"
2166
+ print(f"history for {args.audit_id} (trend: {symbol})")
2167
+ for r in runs:
2168
+ from datetime import datetime, timezone
2169
+ t = datetime.fromtimestamp(r.timestamp, tz=timezone.utc).isoformat()
2170
+ ra = (f" → {r.reaudit_score}"
2171
+ if r.reaudit_score is not None else "")
2172
+ mark = "ok" if r.verified else "FAIL"
2173
+ print(
2174
+ f" [{mark}] {r.run_id} score={r.score}{ra} "
2175
+ f"findings={r.finding_count} workers={r.fix_workers_dispatched} "
2176
+ f"({t})"
2177
+ )
2178
+ return 0
2179
+
2180
+ if sub in ("run", "all"):
2181
+ from omega_engine.audit_arsenal import AuditRegistry
2182
+ from omega_engine.audits.pipeline import AuditPipeline
2183
+ from omega_engine.router import ModelRouter
2184
+ from omega_engine.provider import MockProvider
2185
+
2186
+ audits_dir = home / "Agentik_SSOT" / "audits"
2187
+ if not audits_dir.is_dir():
2188
+ print(f"error: audits dir missing: {audits_dir}")
2189
+ return 1
2190
+ registry = AuditRegistry.load(audits_dir)
2191
+ # MockProvider when no real provider is configured — keeps the CLI
2192
+ # usable on dev machines without an API key. The doctor flags the
2193
+ # provider state separately.
2194
+ router = ModelRouter.single(MockProvider())
2195
+ executor = None
2196
+ if args.fix:
2197
+ from omega_engine.audit import AuditGate
2198
+ from omega_engine.bus import EventBus
2199
+ from omega_engine.executor import Executor
2200
+ from omega_engine.store import SQLiteStore
2201
+ store = SQLiteStore(
2202
+ home / "Agentik_Runtime" / "eventlog" / "omega.db"
2203
+ )
2204
+ bus = EventBus(store)
2205
+ executor = Executor(store, bus, router, AuditGate(),
2206
+ partial_policy="accept_partial")
2207
+
2208
+ pipeline = AuditPipeline(registry, router, omega_home=home,
2209
+ executor=executor)
2210
+
2211
+ if sub == "run":
2212
+ try:
2213
+ outcome = pipeline.run(
2214
+ args.audit_id, scope=args.scope or ".",
2215
+ role=args.role, read_only=not args.fix,
2216
+ max_batches=args.max_workers,
2217
+ min_severity=args.min_severity,
2218
+ )
2219
+ except KeyError as exc:
2220
+ print(f"error: {exc}")
2221
+ return 2
2222
+ _print_outcome(outcome)
2223
+ return 0 if outcome.verdict.verified else 1
2224
+
2225
+ # sub == "all"
2226
+ outcomes = pipeline.run_all_applicable(
2227
+ scope=args.scope or ".", role=args.role,
2228
+ read_only=not args.fix,
2229
+ )
2230
+ any_fail = False
2231
+ for o in outcomes:
2232
+ _print_outcome(o, compact=True)
2233
+ if not o.verdict.verified:
2234
+ any_fail = True
2235
+ return 1 if any_fail else 0
2236
+
2237
+ if sub == "batch":
2238
+ from omega_engine.audit_arsenal import AuditRegistry
2239
+ from omega_engine.audits.batcher import Finding, batch_findings
2240
+ from omega_engine.audits.history import list_runs
2241
+
2242
+ runs = list_runs(home, audit_id=args.audit_id, limit=1)
2243
+ if not runs:
2244
+ print(f"no run recorded for {args.audit_id!r} — run it first")
2245
+ return 1
2246
+ run = runs[0]
2247
+ findings = [
2248
+ Finding.from_dict(f)
2249
+ for f in run.verdict.get("findings", [])
2250
+ ]
2251
+ plan = batch_findings(
2252
+ findings, audit_id=args.audit_id,
2253
+ max_batches=args.max_workers,
2254
+ min_severity=args.min_severity,
2255
+ )
2256
+ print(f"batch plan for {args.audit_id} run {run.run_id}:")
2257
+ for b in plan.batches:
2258
+ print(f" {b.id}: {len(b.findings)} finding(s) across "
2259
+ f"{len(b.files)} file(s) — score={b.score}")
2260
+ for f in b.findings[:5]:
2261
+ print(f" - [{f.severity}] {f.file or '(global)'} — "
2262
+ f"{f.message[:70]}")
2263
+ if len(b.findings) > 5:
2264
+ print(f" ... and {len(b.findings) - 5} more")
2265
+ if plan.deferred:
2266
+ print(f" deferred: {len(plan.deferred)} finding(s) "
2267
+ "(over batch cap)")
2268
+ return 0
2269
+
2270
+ print(f"unknown audit subcommand: {sub}")
2271
+ return 2
2272
+
2273
+
2274
+ def _print_outcome(outcome, *, compact: bool = False) -> None:
2275
+ v = outcome.verdict
2276
+ mark = "ok" if v.verified else "FAIL"
2277
+ head = (f"[{mark}] {outcome.audit_id:<18} score={v.score:>3} "
2278
+ f"findings={len(v.findings):>3} "
2279
+ f"({outcome.elapsed_s}s)")
2280
+ if outcome.reaudit:
2281
+ head += f" reaudit={outcome.reaudit.score:>3}"
2282
+ if compact:
2283
+ print(f" {head}")
2284
+ return
2285
+ print(head)
2286
+ print(f" run_id: {outcome.run_id}")
2287
+ if v.summary:
2288
+ print(f" summary: {v.summary[:200]}")
2289
+ if outcome.plan:
2290
+ for b in outcome.plan.batches:
2291
+ print(f" batch {b.id}: {len(b.findings)} findings, "
2292
+ f"{len(b.files)} file(s)")
2293
+ for d in outcome.dispatches:
2294
+ print(f" dispatch {d.batch_id}: {d.status} "
2295
+ + (f"mission={d.mission_id}" if d.mission_id else d.reason))
2296
+
2297
+
2298
+ def cmd_pursue(args: argparse.Namespace) -> int:
2299
+ """Drive an agent toward a verifiable goal via the engine's pursue loop.
2300
+
2301
+ Agentik OS native equivalent of goal-driven workers (replaces /goal).
2302
+ Uses the Max subscription path: any registered provider (Mock for tests,
2303
+ Claude for real). Writes per-iteration logs and a structured .done.json.
2304
+ """
2305
+ from omega_engine.pursue import pursue
2306
+ result = pursue(
2307
+ intent=args.intent,
2308
+ verify_cmd=args.verify_cmd,
2309
+ omega_home=_omega_home(),
2310
+ max_iterations=args.max_iters,
2311
+ project=args.project,
2312
+ role=args.role,
2313
+ cwd=args.cwd,
2314
+ )
2315
+ print(f"pursue {result.task_id}: {result.status} "
2316
+ f"after {result.iterations} iter(s) (verify_exit={result.final_verify_exit})")
2317
+ print(f" log: {result.log_path}")
2318
+ print(f" done: {result.done_path}")
2319
+ return 0 if result.status == "done_clean" else 1
2320
+
2321
+
2322
+ def cmd_cadence(args: argparse.Namespace) -> int:
2323
+ """Schedule, list, disable, or remove a recurring beat.
2324
+
2325
+ Agentik OS native equivalent of /loop — uses the AutonomousSupervisor
2326
+ so the schedule survives Claude Code sessions and editor restarts.
2327
+ """
2328
+ from omega_engine.cadence import (
2329
+ disable_cadence,
2330
+ list_cadences,
2331
+ remove_cadence,
2332
+ schedule_cadence,
2333
+ )
2334
+ sub = args.cadence_cmd
2335
+ home = _omega_home()
2336
+
2337
+ if sub is None:
2338
+ print("usage: omega cadence {schedule|list|disable|remove} ...")
2339
+ return 2
2340
+ if sub == "schedule":
2341
+ if not (args.trigger and args.schedule and args.intent):
2342
+ print("usage: omega cadence schedule <cron|delay|self> <schedule> <intent>")
2343
+ return 2
2344
+ try:
2345
+ path = schedule_cadence(
2346
+ intent=args.intent,
2347
+ trigger_type=args.trigger,
2348
+ schedule=args.schedule,
2349
+ omega_home=home,
2350
+ )
2351
+ except ValueError as exc:
2352
+ print(f"error: {exc}")
2353
+ return 2
2354
+ print(f"scheduled: {path}")
2355
+ return 0
2356
+ if sub == "list":
2357
+ entries = list_cadences(home)
2358
+ if not entries:
2359
+ print("(no cadences scheduled)")
2360
+ return 0
2361
+ print(f"{len(entries)} cadence(s):")
2362
+ for e in entries:
2363
+ tr = e["trigger"]
2364
+ mark = "ok" if e["enabled"] else "off"
2365
+ print(f" [{mark}] {e['id']} ({tr.get('type', '?')}/{tr.get('schedule', '?')}) → {e['intent'][:60]}")
2366
+ return 0
2367
+ if sub == "disable":
2368
+ if not args.id:
2369
+ print("usage: omega cadence disable <id>")
2370
+ return 2
2371
+ if disable_cadence(args.id, omega_home=home):
2372
+ print(f"disabled {args.id}")
2373
+ return 0
2374
+ print(f"no enabled cadence with id {args.id!r}")
2375
+ return 1
2376
+ if sub == "remove":
2377
+ if not args.id:
2378
+ print("usage: omega cadence remove <id>")
2379
+ return 2
2380
+ if remove_cadence(args.id, omega_home=home):
2381
+ print(f"removed {args.id}")
2382
+ return 0
2383
+ print(f"no cadence with id {args.id!r}")
2384
+ return 1
2385
+ print(f"unknown cadence subcommand: {sub}")
2386
+ return 2
2387
+
2388
+
2389
+ def cmd_daemon(args: argparse.Namespace) -> int:
2390
+ """Run one of the 24/7 service daemons.
2391
+
2392
+ ``omega daemon engine`` — the event-store keeper + local HTTP API.
2393
+ ``omega daemon telegram`` — long-polls Telegram, routes inbound messages.
2394
+ ``omega daemon autonomous`` — runs the autonomous-agent supervisor.
2395
+
2396
+ The installer's systemd user units invoke these.
2397
+ """
2398
+ name = args.name
2399
+ if name == "engine":
2400
+ from omega_engine.daemons import engine as d
2401
+ return d.main()
2402
+ if name == "telegram":
2403
+ from omega_engine.daemons import telegram as d
2404
+ return d.main()
2405
+ if name == "autonomous":
2406
+ from omega_engine.daemons import autonomous as d
2407
+ return d.main()
2408
+ print(f"unknown daemon: {name!r} — expected engine|telegram|autonomous")
2409
+ return 2
2410
+
2411
+
2412
+ def cmd_classify(args: argparse.Namespace) -> int:
2413
+ """`omega classify "<task>"` — show how the router would classify it."""
2414
+ from omega_engine.classifier import classify
2415
+ c = classify(args.task, role=args.role)
2416
+ print(f" bucket: {c.bucket}")
2417
+ print(f" confidence: {c.confidence}")
2418
+ print(f" suggested: {c.suggested_model}")
2419
+ print(f" reason: {c.reason}")
2420
+ return 0
2421
+
2422
+
2423
+ def cmd_memory(args: argparse.Namespace) -> int:
2424
+ """`omega memory {reindex,search,stats}` — semantic memory across sources."""
2425
+ from omega_engine import memory
2426
+ home = _omega_home()
2427
+ sub = args.memory_cmd or "stats"
2428
+ if sub == "reindex":
2429
+ counts = memory.reindex(home)
2430
+ print(f" total: {counts['total']}")
2431
+ print(f" telegram: {counts['telegram']}")
2432
+ print(f" done_json: {counts['done_json']}")
2433
+ print(f" audit: {counts['audit']}")
2434
+ return 0
2435
+ if sub == "search":
2436
+ hits = memory.search(home, args.query, k=args.k)
2437
+ if not hits:
2438
+ print(" (no matches — try `omega memory reindex` first)")
2439
+ return 0
2440
+ from datetime import datetime, timezone
2441
+ for h in hits:
2442
+ t = (datetime.fromtimestamp(h.timestamp, tz=timezone.utc)
2443
+ .strftime("%Y-%m-%d %H:%M")
2444
+ if h.timestamp else "?")
2445
+ print(f" [{h.source:<10}] {h.ref} ({t}) score={h.score:.3f}")
2446
+ for line in h.text.splitlines()[:3]:
2447
+ print(f" {line[:120]}")
2448
+ return 0
2449
+ if sub == "stats":
2450
+ stats = memory.index_stats(home)
2451
+ if not stats["indexed"]:
2452
+ print(" no index yet — run `omega memory reindex`")
2453
+ return 0
2454
+ print(f" indexed: {stats['total']:,d} documents")
2455
+ print(f" size: {stats['size_bytes']:,d} bytes")
2456
+ return 0
2457
+ print(f"unknown memory subcommand: {sub}")
2458
+ return 2
2459
+
2460
+
2461
+ def cmd_learn(args: argparse.Namespace) -> int:
2462
+ """`omega learn {reflect,report}` — Smith reflection."""
2463
+ from omega_engine.learning import reflect
2464
+ home = _omega_home()
2465
+ sub = args.learn_cmd or "reflect"
2466
+ if sub == "reflect":
2467
+ report = reflect(home, write_proposals=args.write)
2468
+ print(f" insights: {len(report.insights)}")
2469
+ for ins in report.insights:
2470
+ print(f"\n [{ins.severity:<4}] {ins.kind}")
2471
+ print(f" ─ {ins.title}")
2472
+ print(f" {ins.detail[:200]}")
2473
+ if ins.proposed_action:
2474
+ print(f" → {ins.proposed_action[:200]}")
2475
+ if args.write and report.insights:
2476
+ print(f"\n proposals written to "
2477
+ f"{home}/Agentik_Extra/staging/promotion/")
2478
+ return 0
2479
+ if sub == "report":
2480
+ # Print the last report (just the structured form).
2481
+ import json as _json
2482
+ report = reflect(home, write_proposals=False)
2483
+ print(_json.dumps(report.to_dict(), indent=2))
2484
+ return 0
2485
+ print(f"unknown learn subcommand: {sub}")
2486
+ return 2
2487
+
2488
+
2489
+ def cmd_message(args: argparse.Namespace) -> int:
2490
+ """`omega message {send,read,wait,open}` — worker ↔ oracle messaging."""
2491
+ from omega_engine import agent_messages as am
2492
+ home = _omega_home()
2493
+ sub = args.message_cmd or "open"
2494
+ if sub == "send":
2495
+ try:
2496
+ msg = am.send(home, args.task_id,
2497
+ from_=args.from_, to=args.to,
2498
+ type=args.type, text=args.text)
2499
+ except ValueError as exc:
2500
+ print(f" error: {exc}")
2501
+ return 2
2502
+ print(f" sent: {msg.from_} → {msg.to} ({msg.type})")
2503
+ return 0
2504
+ if sub == "read":
2505
+ msgs = am.read(home, args.task_id, for_recipient=args.to)
2506
+ for m in msgs:
2507
+ from datetime import datetime, timezone
2508
+ t = datetime.fromtimestamp(m.ts, tz=timezone.utc).strftime("%H:%M:%S")
2509
+ print(f" [{t}] {m.from_} → {m.to} ({m.type}): {m.text[:200]}")
2510
+ return 0
2511
+ if sub == "wait":
2512
+ msg = am.wait_for_reply(home, args.task_id,
2513
+ for_recipient=args.to,
2514
+ timeout_s=args.timeout)
2515
+ if msg is None:
2516
+ print(f" timeout after {args.timeout}s")
2517
+ return 1
2518
+ print(f" reply: {msg.from_} → {msg.to} ({msg.type}): {msg.text}")
2519
+ return 0
2520
+ if sub == "open":
2521
+ opens = am.list_open_questions(home)
2522
+ if not opens:
2523
+ print(" (no open questions)")
2524
+ return 0
2525
+ for task_id, m in opens:
2526
+ from datetime import datetime, timezone
2527
+ t = datetime.fromtimestamp(m.ts, tz=timezone.utc).strftime("%H:%M")
2528
+ print(f" [{task_id}] {t} {m.from_} → {m.to}: {m.text[:120]}")
2529
+ return 0
2530
+ print(f"unknown message subcommand: {sub}")
2531
+ return 2
2532
+
2533
+
2534
+ def cmd_tmux(args: argparse.Namespace) -> int:
2535
+ """`omega tmux` — categorised tmux orchestration.
2536
+
2537
+ Mirrors the live VPS pattern: AISB / oracle / worker / dev / linear /
2538
+ home / other. Spawn sessions via the right convention, attach without
2539
+ blocking the shell, kill cleanly, and run a popup picker bound to
2540
+ prefix+Z in the recommended tmux.conf.
2541
+ """
2542
+ from omega_engine import tmux
2543
+ sub = args.tmux_cmd or "list"
2544
+ home = _omega_home()
2545
+
2546
+ if sub == "list":
2547
+ sessions = tmux.list_sessions(home)
2548
+ if not sessions:
2549
+ print("(no tmux sessions — run `omega tmux spawn <type> <name>` to start one)")
2550
+ return 0
2551
+ for cat, lst in tmux.grouped(sessions).items():
2552
+ if not lst:
2553
+ continue
2554
+ print(f" ── {cat} ────────────────────────────")
2555
+ for s in lst:
2556
+ star = "*" if s.attached else " "
2557
+ proj = f" [{s.project}]" if s.project else ""
2558
+ print(f" {star} {s.name:<50} {s.windows}w{proj}")
2559
+ return 0
2560
+
2561
+ if sub == "count":
2562
+ print(len(tmux.list_sessions(home)))
2563
+ return 0
2564
+
2565
+ if sub == "attach":
2566
+ if not tmux.is_alive(args.name):
2567
+ print(f"no session: {args.name}")
2568
+ return 1
2569
+ cmd = tmux.attach_command(args.name)
2570
+ print(cmd)
2571
+ return 0
2572
+
2573
+ if sub == "kill":
2574
+ if tmux.kill(args.name):
2575
+ print(f"killed: {args.name}")
2576
+ return 0
2577
+ print(f"no such session: {args.name}")
2578
+ return 1
2579
+
2580
+ if sub == "spawn":
2581
+ kind = args.kind
2582
+ name = args.name
2583
+ cwd = args.cwd or os.getcwd()
2584
+ if kind == "oracle":
2585
+ real = tmux.spawn_oracle(name, cwd=cwd)
2586
+ elif kind == "worker":
2587
+ real = tmux.spawn_worker(name, args.task or "task", cwd=cwd)
2588
+ elif kind == "aisb-chat":
2589
+ real = tmux.spawn_aisb_chat(home)
2590
+ else:
2591
+ ok = tmux.spawn(name, cwd=cwd)
2592
+ if not ok:
2593
+ print(f"session already exists: {name}")
2594
+ return 1
2595
+ real = name
2596
+ print(f"spawned: {real}")
2597
+ print(f"attach: {tmux.attach_command(real)}")
2598
+ return 0
2599
+
2600
+ if sub == "health":
2601
+ report = tmux.health_report(home)
2602
+ if not report.get("tmux_installed"):
2603
+ print(" tmux NOT installed — `omega upgrade` or apt install tmux")
2604
+ return 1
2605
+ print(f" tmux installed: yes")
2606
+ print(f" sessions: {report['sessions']} "
2607
+ f"({report.get('attached', 0)} attached)")
2608
+ for cat, names in report["by_category"].items():
2609
+ print(f" {cat:<8} {len(names):>3}")
2610
+ if report["stale"]:
2611
+ print(f" STALE workers (>6h detached): {report['stale']}")
2612
+ return 0
2613
+
2614
+ if sub == "cleanup":
2615
+ result = tmux.cleanup_stale(home, older_than_hours=args.hours,
2616
+ dry_run=not args.yes)
2617
+ verb = "would kill" if result["dry_run"] else "killed"
2618
+ print(f" {verb} {len(result['candidates'])} stale worker session(s)")
2619
+ for n in result["candidates"][:10]:
2620
+ print(f" - {n}")
2621
+ return 0
2622
+
2623
+ if sub == "menu":
2624
+ # Interactive whiptail picker. Designed to be invoked from a
2625
+ # tmux popup (prefix+Z): we print "exec tmux switch-client -t <name>"
2626
+ # which the popup can pipe through.
2627
+ from omega_engine.menu import _have, _whiptail_menu
2628
+ if not _have("whiptail"):
2629
+ print("whiptail required for `omega tmux menu`")
2630
+ return 2
2631
+ sessions = tmux.list_sessions(home)
2632
+ if not sessions:
2633
+ print("(no sessions)")
2634
+ return 0
2635
+ items = []
2636
+ for cat, lst in tmux.grouped(sessions).items():
2637
+ for s in lst:
2638
+ marker = "*" if s.attached else " "
2639
+ items.append((s.name, f"[{cat}]{marker} {s.name[:50]}"))
2640
+ if not items:
2641
+ return 0
2642
+ choice = _whiptail_menu(
2643
+ "Tmux sessions", "Pick a session to attach:", items=items,
2644
+ )
2645
+ if choice is None:
2646
+ return 0
2647
+ # When invoked from a tmux popup, switch-client transitions cleanly.
2648
+ if os.environ.get("TMUX"):
2649
+ subprocess.run(["tmux", "switch-client", "-t", choice], timeout=5)
2650
+ else:
2651
+ print(tmux.attach_command(choice))
2652
+ return 0
2653
+
2654
+ if sub == "config":
2655
+ profile = getattr(args, "profile", "minimal")
2656
+ path = tmux.write_default_config(profile=profile)
2657
+ print(f" wrote tmux config ({profile}): {path}")
2658
+ print()
2659
+ print(" To use it, either:")
2660
+ print(f" source {path} # add this line to your ~/.tmux.conf")
2661
+ print(f" cp {path} ~/.tmux.conf")
2662
+ print()
2663
+ print(" OR run `omega tmux install` to write ~/.tmux.conf "
2664
+ "directly (with backup).")
2665
+ return 0
2666
+
2667
+ if sub == "install":
2668
+ profile = getattr(args, "profile", "pro")
2669
+ backup = not getattr(args, "no_backup", False)
2670
+ res = tmux.install_into_home_tmux_conf(
2671
+ profile=profile, backup=backup,
2672
+ )
2673
+ print(f" ✓ ~/.tmux.conf installed (profile: {res['profile']})")
2674
+ if res["backup_path"]:
2675
+ print(f" backup → {res['backup_path']}")
2676
+ print(f" bundled → {res['bundled_copy']}")
2677
+ print()
2678
+ print(" Reload tmux NOW:")
2679
+ print(" - In a running tmux: prefix+r (or tmux source-file ~/.tmux.conf)")
2680
+ print(" - Or just: tmux kill-server and start fresh")
2681
+ print()
2682
+ print(" Try the picker: tmux → prefix+Z (or `omega tmux menu`)")
2683
+ return 0
2684
+
2685
+ print(f"unknown tmux subcommand: {sub}")
2686
+ return 2
2687
+
2688
+
2689
+ def cmd_aisb(args: argparse.Namespace) -> int:
2690
+ """`omega aisb` — talk to the AISB master agent without Telegram.
2691
+
2692
+ Two sub-commands:
2693
+ chat — spawn (or attach to) the AISB-chat tmux session
2694
+ chat-loop — the REPL loop itself (run inside the tmux session)
2695
+ """
2696
+ from omega_engine import tmux
2697
+ sub = args.aisb_cmd or "chat"
2698
+ home = _omega_home()
2699
+ if sub == "chat":
2700
+ name = "AISB-chat"
2701
+ if tmux.is_alive(name):
2702
+ print(f" session already exists: {name}")
2703
+ else:
2704
+ tmux.spawn_aisb_chat(home)
2705
+ print(f" spawned: {name}")
2706
+ print(f" attach: {tmux.attach_command(name)}")
2707
+ return 0
2708
+ if sub == "chat-loop":
2709
+ from omega_engine.aisb_chat import run_chat_loop
2710
+ return run_chat_loop()
2711
+ print(f"unknown aisb subcommand: {sub}")
2712
+ return 2
2713
+
2714
+
2715
+ def cmd_menu(_args: argparse.Namespace) -> int:
2716
+ """`omega` with no args → interactive whiptail menu (the on-ramp)."""
2717
+ from omega_engine.menu import run_menu
2718
+ return run_menu()
2719
+
2720
+
2721
+ def cmd_hermes_desktop(args: argparse.Namespace) -> int:
2722
+ """`omega hermes-desktop {install,serve,profile,status}` — expose this
2723
+ VPS as a remote backend for the Hermes Desktop Electron app."""
2724
+ import json as _json
2725
+ from omega_engine import hermes_desktop as HD
2726
+ sub = getattr(args, "hd_cmd", None)
2727
+ home = _omega_home()
2728
+
2729
+ if sub == "install" or sub is None:
2730
+ tok = HD.generate_token(home)
2731
+ prof = HD.build_profile(
2732
+ public_url=getattr(args, "public_url", None),
2733
+ port=args.port, omega_home=home,
2734
+ )
2735
+ print(f" ✓ token generated: {HD.token_path(home)}")
2736
+ print()
2737
+ print(" Paste this profile into Hermes Desktop:")
2738
+ print(f" Name: {prof.name}")
2739
+ print(f" URL: {prof.base_url}")
2740
+ print(f" API key: {prof.api_key}")
2741
+ print()
2742
+ print(" Then start the server on the VPS:")
2743
+ print(f" omega hermes-desktop serve --port {args.port}")
2744
+ print()
2745
+ print(" Recommended deployment: tmux session (survives SSH disconnect)")
2746
+ print(f" tmux new -d -s omega-hd 'omega hermes-desktop serve --port {args.port}'")
2747
+ return 0
2748
+
2749
+ if sub == "profile":
2750
+ prof = HD.build_profile(
2751
+ public_url=getattr(args, "public_url", None),
2752
+ port=args.port, omega_home=home,
2753
+ )
2754
+ print(prof.to_json())
2755
+ return 0
2756
+
2757
+ if sub == "status":
2758
+ tok = HD.read_token(home)
2759
+ free = HD.is_port_free(args.port)
2760
+ print(f" token: {'✓ present' if tok else '✗ run install first'}"
2761
+ f" ({HD.token_path(home)})")
2762
+ print(f" port {args.port}: {'free (server not running)' if free else 'in use (server up)'}")
2763
+ return 0
2764
+
2765
+ if sub == "serve":
2766
+ host = "0.0.0.0" if getattr(args, "unsafe_public", False) \
2767
+ else "127.0.0.1"
2768
+ print(f" serving Hermes-Desktop adapter on {host}:{args.port}")
2769
+ if host == "127.0.0.1":
2770
+ print(" (loopback only — use `ssh -L 8642:127.0.0.1:8642 ...` "
2771
+ "for desktop access)")
2772
+ print(f" token: {HD.token_path(home)}")
2773
+ print(" press Ctrl-C to stop")
2774
+ HD.serve_forever(
2775
+ host=host, port=args.port, omega_home=home,
2776
+ quiet=False,
2777
+ unsafe_public=getattr(args, "unsafe_public", False),
2778
+ )
2779
+ return 0
2780
+
2781
+ print("usage: omega hermes-desktop {install|serve|profile|status}",
2782
+ file=sys.stderr)
2783
+ return 2
2784
+
2785
+
2786
+ def cmd_hermes(args: argparse.Namespace) -> int:
2787
+ """`omega hermes {install,link,status,ask,env}` — Layer-2 Hermes bridge.
2788
+
2789
+ Hermes is the Nous Research autonomous agent runtime. We bridge it
2790
+ so it uses the SAME Claude Max OAuth as ``claude`` CLI — no API key,
2791
+ no separate auth, no extra credential to manage. Token rotates
2792
+ server-side; bridge reads ``~/.claude/.credentials.json`` live on
2793
+ every call so refreshes propagate naturally.
2794
+ """
2795
+ from omega_engine import hermes as H
2796
+ import json as _json
2797
+ sub = getattr(args, "hermes_cmd", None)
2798
+
2799
+ if sub == "status":
2800
+ st = H.status()
2801
+ if getattr(args, "json", False):
2802
+ print(_json.dumps({
2803
+ "installed": st.installed,
2804
+ "hermes_bin": st.hermes_bin,
2805
+ "hermes_version": st.hermes_version,
2806
+ "hermes_home": st.hermes_home,
2807
+ "omega_env_present": st.omega_env_present,
2808
+ "oauth_token_present": st.oauth_token_present,
2809
+ "oauth_subscription": st.oauth_subscription,
2810
+ "oauth_expires_at_ms": st.oauth_expires_at_ms,
2811
+ "oauth_expires_in_s": st.oauth_expires_in_s,
2812
+ }, indent=2))
2813
+ return 0
2814
+ marks = {"ok": "✓", "no": "✗"}
2815
+ print(f" hermes installed: {marks['ok'] if st.installed else marks['no']} "
2816
+ f"{st.hermes_bin or '(not on PATH)'}")
2817
+ if st.hermes_version:
2818
+ print(f" hermes version: {st.hermes_version}")
2819
+ print(f" hermes home: {st.hermes_home}")
2820
+ print(f" omega.env linked: {marks['ok'] if st.omega_env_present else marks['no']}")
2821
+ print(f" Claude OAuth token: {marks['ok'] if st.oauth_token_present else marks['no']}"
2822
+ f" ({st.oauth_subscription or 'unknown sub'})")
2823
+ if st.oauth_expires_in_s is not None:
2824
+ hours = st.oauth_expires_in_s / 3600
2825
+ print(f" token expires in: {hours:.1f} h")
2826
+ if not st.installed:
2827
+ print()
2828
+ print(" next: omega hermes install")
2829
+ elif not st.omega_env_present:
2830
+ print()
2831
+ print(" next: omega hermes link")
2832
+ return 0 if st.installed and st.omega_env_present and st.oauth_token_present else 1
2833
+
2834
+ if sub == "install":
2835
+ res = H.install(
2836
+ skip_browser=not getattr(args, "with_browser", False),
2837
+ skip_setup=True,
2838
+ yes=getattr(args, "yes", False),
2839
+ dry_run=getattr(args, "dry_run", False),
2840
+ timeout_s=args.timeout,
2841
+ )
2842
+ if getattr(args, "json", False):
2843
+ print(_json.dumps({
2844
+ "ok": res.ok, "method": res.method,
2845
+ "elapsed_s": res.elapsed_s, "detail": res.detail,
2846
+ "error": res.error,
2847
+ }, indent=2))
2848
+ return 0 if res.ok else 1
2849
+ mark = "✓" if res.ok else "✗"
2850
+ print(f" {mark} {res.method} ({res.elapsed_s}s)")
2851
+ if res.detail:
2852
+ print(f" {res.detail}")
2853
+ if res.error:
2854
+ print(f" {res.error}")
2855
+ return 0 if res.ok else 1
2856
+
2857
+ if sub == "link":
2858
+ res = H.link(
2859
+ provider=args.provider, model=args.model,
2860
+ dry_run=getattr(args, "dry_run", False),
2861
+ )
2862
+ if getattr(args, "json", False):
2863
+ print(_json.dumps({
2864
+ "ok": res.ok, "config_path": res.config_path,
2865
+ "provider": res.provider, "model": res.model,
2866
+ "expires_at_ms": res.expires_at_ms,
2867
+ "subscription": res.subscription,
2868
+ "error": res.error,
2869
+ }, indent=2))
2870
+ return 0 if res.ok else 1
2871
+ mark = "✓" if res.ok else "✗"
2872
+ print(f" {mark} linked provider={res.provider} model={res.model}")
2873
+ if res.config_path:
2874
+ print(f" wrote {res.config_path}")
2875
+ if res.subscription:
2876
+ print(f" Claude subscription: {res.subscription}")
2877
+ if res.error:
2878
+ print(f" {res.error}")
2879
+ return 0 if res.ok else 1
2880
+
2881
+ if sub == "ask":
2882
+ try:
2883
+ rc, stdout, stderr = H.ask(args.prompt, timeout_s=args.timeout)
2884
+ except H.HermesError as exc:
2885
+ print(f"error: {exc}", file=sys.stderr)
2886
+ return 2
2887
+ if stdout:
2888
+ print(stdout, end="" if stdout.endswith("\n") else "\n")
2889
+ if rc != 0 and stderr:
2890
+ print(stderr, file=sys.stderr)
2891
+ return rc
2892
+
2893
+ if sub == "env":
2894
+ # Print the env we'd hand to hermes — useful for `eval $(...)`
2895
+ try:
2896
+ env = H.build_env()
2897
+ except H.HermesError as exc:
2898
+ print(f"error: {exc}", file=sys.stderr)
2899
+ return 2
2900
+ for k in ("CLAUDE_CODE_OAUTH_TOKEN", "HERMES_PROVIDER", "HERMES_MODEL"):
2901
+ if k in env:
2902
+ if k == "CLAUDE_CODE_OAUTH_TOKEN":
2903
+ print(f"export {k}=<live-from-{H._CLAUDE_CREDS}>")
2904
+ else:
2905
+ print(f"export {k}={env[k]!r}")
2906
+ return 0
2907
+
2908
+ if sub == "brief":
2909
+ # Teach Hermès what OmegaOS is + observe outcomes for the
2910
+ # self-improvement loop. Writes a SKILL.md inside ~/.hermes/.
2911
+ from omega_engine import hermes_bootstrap as HB
2912
+ op = getattr(args, "brief_op", "status")
2913
+ if op == "write":
2914
+ res = HB.write_brief()
2915
+ print(f" ✓ wrote OmegaOS brief to {res.path}")
2916
+ print(f" {res.bytes_written} bytes — Hermès reads it "
2917
+ f"on next invocation")
2918
+ print(f" omega_home: {res.omega_home}")
2919
+ return 0
2920
+ if op == "status":
2921
+ st = HB.status()
2922
+ mark = lambda b: "✓" if b else "✗"
2923
+ print(f" {mark(st['brief_present'])} brief at {st['brief_path']}"
2924
+ f" ({st['brief_size']} bytes)")
2925
+ print(f" {mark(st['memory_log_present'])} memory log "
2926
+ f"({st['observation_count']} observations)")
2927
+ if st["last_observed_at"]:
2928
+ import time as _t
2929
+ age = _t.time() - st["last_observed_at"]
2930
+ print(f" last observation: {age/60:.1f} min ago")
2931
+ return 0 if st["brief_present"] else 1
2932
+ if op == "observe":
2933
+ res = HB.observe_outcome(args.slug, args.task_id)
2934
+ if res is None:
2935
+ print(f" no .done.json yet for task {args.task_id}",
2936
+ file=sys.stderr)
2937
+ return 1
2938
+ print(f" ✓ recorded {res.task_id} → {res.status}")
2939
+ return 0
2940
+ print(f"unknown brief op: {op}", file=sys.stderr)
2941
+ return 2
2942
+
2943
+ print("usage: omega hermes {status|install|link|ask|env|brief} ...",
2944
+ file=sys.stderr)
2945
+ return 2
2946
+
2947
+
2948
+ def cmd_ua(args: argparse.Namespace) -> int:
2949
+ """`omega ua {install,run,consume}` — Understand-Anything bridge.
2950
+
2951
+ Claude Code plugin that builds an interactive knowledge graph of a
2952
+ codebase. We bridge install (uses Claude Max OAuth) + the
2953
+ ``/understand`` run + a consumer that lands the resulting graph
2954
+ into OmegaOS's GraphRetriever.
2955
+ """
2956
+ from omega_engine import understand_anything as UA
2957
+ import json as _json
2958
+ sub = getattr(args, "ua_cmd", None)
2959
+
2960
+ if sub == "install":
2961
+ res = UA.install_plugin(
2962
+ timeout_s=args.timeout,
2963
+ dry_run=getattr(args, "dry_run", False),
2964
+ )
2965
+ if getattr(args, "json", False):
2966
+ print(_json.dumps({
2967
+ "ok": res.ok, "method": res.method,
2968
+ "detail": res.detail, "error": res.error,
2969
+ }, indent=2))
2970
+ return 0 if res.ok else 1
2971
+ mark = "✓" if res.ok else "✗"
2972
+ print(f" {mark} {res.detail or res.error}")
2973
+ return 0 if res.ok else 1
2974
+
2975
+ if sub == "run":
2976
+ res = UA.run(args.project, timeout_s=args.timeout,
2977
+ language=getattr(args, "language", None))
2978
+ if getattr(args, "json", False):
2979
+ print(_json.dumps({
2980
+ "ok": res.ok, "output_path": res.output_path,
2981
+ "nodes": res.nodes, "edges": res.edges,
2982
+ "elapsed_s": res.elapsed_s, "error": res.error,
2983
+ }, indent=2))
2984
+ return 0 if res.ok else 1
2985
+ mark = "✓" if res.ok else "✗"
2986
+ print(f" {mark} /understand on {args.project} ({res.elapsed_s}s)")
2987
+ if res.ok:
2988
+ print(f" graph: {res.output_path}")
2989
+ print(f" {res.nodes} nodes, {res.edges} edges")
2990
+ print(f" next: omega ua consume {args.project}")
2991
+ else:
2992
+ print(f" {res.error}")
2993
+ return 0 if res.ok else 1
2994
+
2995
+ if sub == "consume":
2996
+ try:
2997
+ info = UA.consume_graph(args.project)
2998
+ except UA.UnderstandAnythingError as exc:
2999
+ print(f"error: {exc}", file=sys.stderr)
3000
+ return 1
3001
+ print(f" ✓ consumed {info['nodes']} nodes, {info['edges']} edges")
3002
+ print(f" written to {info['written_to']}")
3003
+ return 0
3004
+
3005
+ print("usage: omega ua {install|run|consume} ...", file=sys.stderr)
3006
+ return 2
3007
+
3008
+
3009
+ def cmd_genesis(args: argparse.Namespace) -> int:
3010
+ """`omega genesis {new,run,status,resume,stack,reset}` — project genesis pipeline.
3011
+
3012
+ Programmatic 7-phase walker: stack → vision → market (5 files) →
3013
+ branding (3) → PRD (9) → features (N with depends_on) → handoff to plan.
3014
+ Strict isolation per project; resume-safe; no cross-project leakage.
3015
+ """
3016
+ import json as _json
3017
+ from omega_engine.genesis import (
3018
+ GenesisOrchestrator, PHASES, init_project, load_state, pick_stack,
3019
+ stack_questions, project_root,
3020
+ )
3021
+ from omega_engine.genesis.state import StackConfig, delete_project
3022
+
3023
+ sub = getattr(args, "genesis_cmd", None)
3024
+ home = _omega_home()
3025
+
3026
+ if sub == "new":
3027
+ # Build a StackConfig from --stack-* flags if any, else use defaults.
3028
+ answers: dict[str, str] = {}
3029
+ for k in ("type", "frontend", "backend", "deploy", "payments",
3030
+ "ui_kit", "auth"):
3031
+ v = getattr(args, f"stack_{k}", None)
3032
+ if v:
3033
+ answers[k] = v
3034
+ cfg = pick_stack(
3035
+ answers=answers, intent=args.intent or "",
3036
+ accept_defaults=getattr(args, "accept_defaults", False)
3037
+ and not answers,
3038
+ )
3039
+ try:
3040
+ state = init_project(
3041
+ args.slug,
3042
+ name=args.name or args.slug,
3043
+ intent=args.intent or "",
3044
+ stack=cfg,
3045
+ force=getattr(args, "force", False),
3046
+ )
3047
+ except (FileExistsError, ValueError) as exc:
3048
+ print(f"error: {exc}", file=sys.stderr)
3049
+ return 2
3050
+ if getattr(args, "json", False):
3051
+ print(_json.dumps({
3052
+ "slug": state.slug, "name": state.name,
3053
+ "root": str(project_root(state.slug)),
3054
+ "phase": state.phase,
3055
+ "stack": state.stack.to_dict(),
3056
+ }, indent=2))
3057
+ else:
3058
+ print(f" ✓ project '{state.slug}' initialised")
3059
+ print(f" root: {project_root(state.slug)}")
3060
+ print(f" stack: {state.stack.type} · "
3061
+ f"{state.stack.frontend} · {state.stack.backend} · "
3062
+ f"{state.stack.deploy} · {state.stack.ui_kit}")
3063
+ print(f" next: omega genesis run {state.slug}")
3064
+ return 0
3065
+
3066
+ if sub == "questions":
3067
+ # Print the picker's question list (for the Telegram bot / Claude
3068
+ # Code skill to render an interactive form).
3069
+ qs = stack_questions(intent=args.intent or "")
3070
+ if getattr(args, "json", False):
3071
+ print(_json.dumps([{
3072
+ "key": q.key, "header": q.header,
3073
+ "question": q.question, "options": q.options,
3074
+ "recommended": q.recommended, "why": q.why,
3075
+ } for q in qs], indent=2))
3076
+ else:
3077
+ for q in qs:
3078
+ print(f" [{q.header}] {q.question}")
3079
+ print(f" options: {', '.join(q.options)}")
3080
+ print(f" recommended: {q.recommended} — {q.why}")
3081
+ print()
3082
+ return 0
3083
+
3084
+ if sub == "status":
3085
+ orch = GenesisOrchestrator(args.slug)
3086
+ st = orch.status()
3087
+ if getattr(args, "json", False):
3088
+ print(_json.dumps(st, indent=2))
3089
+ return 0
3090
+ if not st["exists"]:
3091
+ print(f" no genesis state for {args.slug!r} — "
3092
+ f"run `omega genesis new {args.slug}`")
3093
+ return 1
3094
+ print(f" {st['slug']} · {st['name']}")
3095
+ print(f" phase: {st['phase']} "
3096
+ f"({len(st['completed_phases'])}/{len(PHASES)} complete)")
3097
+ print(f" done : {', '.join(st['completed_phases']) or '—'}")
3098
+ print(f" next : {', '.join(st['remaining_phases']) or '(complete)'}")
3099
+ print(f" stack: {st['stack']['type']} · "
3100
+ f"{st['stack']['frontend']} · {st['stack']['backend']}")
3101
+ print(f" root : {st['root']}")
3102
+ return 0
3103
+
3104
+ if sub == "run":
3105
+ orch = GenesisOrchestrator(args.slug)
3106
+ # `run` by default advances to completion. `--phase X` runs only
3107
+ # phase X. `--max N` caps iterations.
3108
+ if getattr(args, "phase", None):
3109
+ res = orch.run(only=args.phase)
3110
+ mark = "✓" if res.ok else "✗"
3111
+ print(f" {mark} {res.phase:10s} "
3112
+ f"{len(res.files_written)} files {res.elapsed_s}s "
3113
+ f"{res.error or ''}")
3114
+ return 0 if res.ok else 1
3115
+ results = orch.run_all(max_phases=args.max)
3116
+ if not results:
3117
+ print(" (pipeline already complete)")
3118
+ return 0
3119
+ for r in results:
3120
+ mark = "✓" if r.ok else "✗"
3121
+ print(f" {mark} {r.phase:10s} "
3122
+ f"{len(r.files_written)} files {r.elapsed_s}s "
3123
+ f"{r.error or ''}")
3124
+ return 0 if all(r.ok for r in results) else 1
3125
+
3126
+ if sub == "delete":
3127
+ if not getattr(args, "yes", False):
3128
+ print(f"error: pass --yes to confirm deletion of {args.slug!r}",
3129
+ file=sys.stderr)
3130
+ return 2
3131
+ ok = delete_project(args.slug)
3132
+ print(f" {'✓ deleted' if ok else '(nothing to delete)'} "
3133
+ f"{args.slug}")
3134
+ return 0
3135
+
3136
+ print("usage: omega genesis {new|run|status|questions|delete} ...",
3137
+ file=sys.stderr)
3138
+ return 2
3139
+
3140
+
3141
+ def cmd_plan(args: argparse.Namespace) -> int:
3142
+ """`omega plan {build,next,status,advance,run,retry,list}` — FSM-enforced
3143
+ sequential plan executor. Replaces v6 ``/planner`` which skipped tasks.
3144
+
3145
+ Every state transition goes through the reducer, which refuses to mark
3146
+ a task in-progress / verified if any of its ``depends_on`` aren't yet
3147
+ VERIFIED. The agent CAN'T jump from T-005 to T-090 because the
3148
+ engine returns IllegalTransition.
3149
+ """
3150
+ import json as _json
3151
+ from omega_engine import plan as P
3152
+
3153
+ sub = getattr(args, "plan_cmd", None)
3154
+
3155
+ if sub == "build":
3156
+ try:
3157
+ res = P.build_plan(args.slug,
3158
+ reset_failed=getattr(args, "reset_failed", False))
3159
+ except P.PlanError as exc:
3160
+ print(f"error: {exc}", file=sys.stderr)
3161
+ return 1
3162
+ print(f" ✓ plan built inserted={res['inserted']} "
3163
+ f"updated={res['updated']} total={res['total']}")
3164
+ return 0
3165
+
3166
+ if sub == "next":
3167
+ t = P.next_eligible(args.slug)
3168
+ if t is None:
3169
+ print(f" (no eligible task — plan may be complete or blocked)")
3170
+ return 0
3171
+ if getattr(args, "json", False):
3172
+ print(_json.dumps(t.to_dict(), indent=2))
3173
+ return 0
3174
+ print(f" {t.id} · {t.title}")
3175
+ print(f" wave: {t.wave} · estimated: {t.estimated_minutes}min")
3176
+ print(f" deps: {', '.join(t.depends_on) or '(none — root task)'}")
3177
+ print(f" files: {', '.join(t.files_owned[:3])}"
3178
+ + (f" + {len(t.files_owned)-3} more"
3179
+ if len(t.files_owned) > 3 else ""))
3180
+ print(f" verify: {t.verify_cmd}")
3181
+ print(f" skill: {t.skill}")
3182
+ return 0
3183
+
3184
+ if sub == "status":
3185
+ st = P.status(args.slug)
3186
+ if getattr(args, "json", False):
3187
+ print(_json.dumps(st, indent=2))
3188
+ return 0
3189
+ c = st["counts"]
3190
+ print(f" {st['slug']} · {st['completion_pct']}% complete")
3191
+ print(f" total: {st['total']} · verified: {c['verified']} · "
3192
+ f"in_progress: {c['in_progress']} · pending: {c['pending']} · "
3193
+ f"failed: {c['failed']}")
3194
+ if st["active"]:
3195
+ print(f" active: {st['active']['id']} · {st['active']['title']}")
3196
+ return 0
3197
+
3198
+ if sub == "list":
3199
+ tasks = P.list_tasks(args.slug, state=getattr(args, "state", None))
3200
+ if getattr(args, "json", False):
3201
+ print(_json.dumps([t.to_dict() for t in tasks], indent=2))
3202
+ return 0
3203
+ for t in tasks:
3204
+ mark = {"verified": "✓", "failed": "✗",
3205
+ "in_progress": "▶", "claimed_done": "?",
3206
+ "pending": "·"}.get(t.state, "?")
3207
+ print(f" {mark} {t.id} [{t.state:13s}] "
3208
+ f"w={t.wave} deps={t.depends_on or '[]'} {t.title[:50]}")
3209
+ return 0
3210
+
3211
+ if sub in ("start", "in-progress", "in_progress"):
3212
+ try:
3213
+ t = P.mark_in_progress(args.slug, args.task_id)
3214
+ except P.PlanError as exc:
3215
+ print(f"error: {exc}", file=sys.stderr)
3216
+ return 1
3217
+ print(f" ▶ {t.id} → {t.state}")
3218
+ return 0
3219
+
3220
+ if sub == "claimed":
3221
+ try:
3222
+ t = P.mark_claimed_done(args.slug, args.task_id)
3223
+ except P.PlanError as exc:
3224
+ print(f"error: {exc}", file=sys.stderr)
3225
+ return 1
3226
+ print(f" ? {t.id} → {t.state} (verify pending)")
3227
+ return 0
3228
+
3229
+ if sub == "done":
3230
+ try:
3231
+ t = P.mark_done(args.slug, args.task_id,
3232
+ verify=not getattr(args, "no_verify", False))
3233
+ except P.PlanError as exc:
3234
+ print(f"error: {exc}", file=sys.stderr)
3235
+ return 1
3236
+ mark = "✓" if t.state == P.VERIFIED else "✗"
3237
+ print(f" {mark} {t.id} → {t.state}"
3238
+ + (f" ({t.last_error[:200]})" if t.last_error else ""))
3239
+ return 0 if t.state == P.VERIFIED else 1
3240
+
3241
+ if sub == "fail":
3242
+ try:
3243
+ t = P.mark_failed(args.slug, args.task_id, reason=args.reason or "")
3244
+ except P.PlanError as exc:
3245
+ print(f"error: {exc}", file=sys.stderr)
3246
+ return 1
3247
+ print(f" ✗ {t.id} → {t.state} ({t.last_error})")
3248
+ return 0
3249
+
3250
+ if sub == "retry":
3251
+ try:
3252
+ t = P.retry(args.slug, args.task_id)
3253
+ except P.PlanError as exc:
3254
+ print(f"error: {exc}", file=sys.stderr)
3255
+ return 1
3256
+ print(f" ↻ {t.id} → {t.state}")
3257
+ return 0
3258
+
3259
+ if sub == "run":
3260
+ # Multi-day autonomous loop. The engine walks next_eligible →
3261
+ # in_progress → dispatch → claimed_done → verify → verified.
3262
+ # Reducer enforces ordering; failed tasks naturally cascade-block
3263
+ # their dependants (the right behaviour — operator decides via
3264
+ # retry / fail).
3265
+ spawn = None
3266
+ if args.spawn_cmd:
3267
+ import shlex
3268
+ spawn = shlex.split(args.spawn_cmd)
3269
+ def _print_progress(task, result):
3270
+ if args.json:
3271
+ import json as _j
3272
+ print(_j.dumps({"task_id": task.id, "result": result}))
3273
+ else:
3274
+ mark = {"verified": "✓"}.get(result.split(":")[0], "✗")
3275
+ print(f" {mark} {task.id} {result}")
3276
+ try:
3277
+ result = P.run(
3278
+ args.slug,
3279
+ max_iterations=args.max,
3280
+ spawn_cmd=spawn,
3281
+ on_iteration=_print_progress,
3282
+ sleep_between_s=args.sleep,
3283
+ )
3284
+ except P.PlanError as exc:
3285
+ print(f"error: {exc}", file=sys.stderr)
3286
+ return 1
3287
+ if args.json:
3288
+ print(_json.dumps(result.to_dict(), indent=2))
3289
+ else:
3290
+ print()
3291
+ print(f" loop done — {result.iterations} iterations in "
3292
+ f"{result.elapsed_s}s")
3293
+ print(f" verified: {len(result.verified)} "
3294
+ f"failed: {len(result.failed)}")
3295
+ fs = result.final_status["counts"]
3296
+ print(f" status: verified={fs['verified']} "
3297
+ f"pending={fs['pending']} failed={fs['failed']} "
3298
+ f"({result.final_status['completion_pct']}% complete)")
3299
+ return 0 if not result.failed else 1
3300
+
3301
+ print("usage: omega plan {build|next|status|list|start|claimed|"
3302
+ "done|fail|retry|run} ...", file=sys.stderr)
3303
+ return 2
3304
+
3305
+
3306
+ def cmd_ma(args: argparse.Namespace) -> int:
3307
+ """`omega ma {agent,env,session,run}` — Anthropic Managed Agents lifecycle.
3308
+
3309
+ OPT-IN provider. Requires ANTHROPIC_API_KEY. NOT a substitute for
3310
+ ClaudeMax (Max OAuth, no API key); rather a complement for
3311
+ server-side / long-running / async workloads.
3312
+ """
3313
+ import json as _json
3314
+ sub = getattr(args, "ma_cmd", None)
3315
+
3316
+ # Guard: every subcommand needs an API key.
3317
+ api_key = os.environ.get("ANTHROPIC_API_KEY", "")
3318
+ if not api_key and sub != "info":
3319
+ print("error: ANTHROPIC_API_KEY not set — Managed Agents requires an API key",
3320
+ file=sys.stderr)
3321
+ print(" see: https://platform.claude.com/docs/en/managed-agents/overview",
3322
+ file=sys.stderr)
3323
+ return 2
3324
+
3325
+ if sub == "info":
3326
+ print("Managed Agents (Anthropic beta) integration")
3327
+ print(" api key present:", "yes" if api_key else "no (export ANTHROPIC_API_KEY)")
3328
+ print(" beta header: ", "managed-agents-2026-04-01")
3329
+ print(" docs: ",
3330
+ "https://platform.claude.com/docs/en/managed-agents/overview")
3331
+ from omega_engine.managed_agent import ManagedAgentCache
3332
+ cache = ManagedAgentCache(_omega_home())
3333
+ d = cache._load()
3334
+ print(f" cached agents: {len(d['agents'])} {list(d['agents'].keys())}")
3335
+ print(f" cached envs: {len(d['environments'])} {list(d['environments'].keys())}")
3336
+ return 0
3337
+
3338
+ from omega_engine.managed_agent import ManagedAgentClient, ManagedAgentCache
3339
+ client = ManagedAgentClient(api_key=api_key)
3340
+ cache = ManagedAgentCache(_omega_home())
3341
+
3342
+ # ----- agent subcommands -----
3343
+ if sub == "agent":
3344
+ op = getattr(args, "agent_op", "list")
3345
+ if op == "create":
3346
+ agent = client.create_agent(
3347
+ name=args.name, model=args.model,
3348
+ system=args.system or "",
3349
+ )
3350
+ cache.set_agent(args.name, agent["id"])
3351
+ print(_json.dumps({
3352
+ "id": agent["id"], "name": agent.get("name", args.name),
3353
+ "model": agent.get("model"),
3354
+ "version": agent.get("version"),
3355
+ "cached_as": args.name,
3356
+ }, indent=2))
3357
+ return 0
3358
+ if op == "get":
3359
+ print(_json.dumps(client.get_agent(args.id), indent=2))
3360
+ return 0
3361
+ if op == "list":
3362
+ data = client.list_agents(limit=args.limit)
3363
+ if args.json:
3364
+ print(_json.dumps(data, indent=2))
3365
+ return 0
3366
+ rows = data.get("data", []) if isinstance(data, dict) else []
3367
+ print(f"## {len(rows)} agents")
3368
+ for a in rows:
3369
+ print(f" {a.get('id'):42s} {a.get('name')} ({a.get('model')})")
3370
+ return 0
3371
+ if op == "delete":
3372
+ client.delete_agent(args.id)
3373
+ print(f"deleted {args.id}")
3374
+ return 0
3375
+ print(f"unknown agent op: {op}", file=sys.stderr)
3376
+ return 2
3377
+
3378
+ # ----- environment subcommands -----
3379
+ if sub == "env":
3380
+ op = getattr(args, "env_op", "list")
3381
+ if op == "create":
3382
+ env = client.create_environment(name=args.name)
3383
+ cache.set_environment(args.name, env["id"])
3384
+ print(_json.dumps({
3385
+ "id": env["id"], "name": env.get("name", args.name),
3386
+ "cached_as": args.name,
3387
+ }, indent=2))
3388
+ return 0
3389
+ if op == "get":
3390
+ print(_json.dumps(client.get_environment(args.id), indent=2))
3391
+ return 0
3392
+ if op == "list":
3393
+ data = client.list_environments(limit=args.limit)
3394
+ if args.json:
3395
+ print(_json.dumps(data, indent=2))
3396
+ return 0
3397
+ rows = data.get("data", []) if isinstance(data, dict) else []
3398
+ print(f"## {len(rows)} environments")
3399
+ for e in rows:
3400
+ print(f" {e.get('id'):42s} {e.get('name')}")
3401
+ return 0
3402
+ if op == "delete":
3403
+ client.delete_environment(args.id)
3404
+ print(f"deleted {args.id}")
3405
+ return 0
3406
+ print(f"unknown env op: {op}", file=sys.stderr)
3407
+ return 2
3408
+
3409
+ # ----- session subcommands -----
3410
+ if sub == "session":
3411
+ op = getattr(args, "session_op", "list")
3412
+ if op == "create":
3413
+ agent_id = cache.get_agent(args.agent) or args.agent
3414
+ env_id = cache.get_environment(args.env) or args.env
3415
+ s = client.create_session(
3416
+ agent_id=agent_id, environment_id=env_id,
3417
+ title=args.title or "",
3418
+ )
3419
+ print(_json.dumps({
3420
+ "id": s["id"], "agent_id": agent_id,
3421
+ "environment_id": env_id, "title": args.title,
3422
+ }, indent=2))
3423
+ return 0
3424
+ if op == "send":
3425
+ client.send_user_message(args.id, args.text)
3426
+ print("sent")
3427
+ return 0
3428
+ if op == "stream":
3429
+ for ev in client.stream_events(args.id, timeout_s=args.timeout):
3430
+ t = ev.get("type", "?")
3431
+ if t == "agent.message":
3432
+ for b in ev.get("content", []):
3433
+ if b.get("type") == "text":
3434
+ print(b.get("text", ""), end="", flush=True)
3435
+ elif t == "agent.tool_use":
3436
+ print(f"\n[tool: {ev.get('name')}]", flush=True)
3437
+ elif t == "session.status_idle":
3438
+ print("\n[idle]")
3439
+ return 0
3440
+ return 0
3441
+ if op == "list":
3442
+ data = client.list_sessions(limit=args.limit)
3443
+ if args.json:
3444
+ print(_json.dumps(data, indent=2))
3445
+ return 0
3446
+ rows = data.get("data", []) if isinstance(data, dict) else []
3447
+ print(f"## {len(rows)} sessions")
3448
+ for s in rows:
3449
+ print(f" {s.get('id'):42s} {s.get('title','')}")
3450
+ return 0
3451
+ if op == "get":
3452
+ print(_json.dumps(client.get_session(args.id), indent=2))
3453
+ return 0
3454
+ if op == "delete":
3455
+ client.delete_session(args.id)
3456
+ print(f"deleted {args.id}")
3457
+ return 0
3458
+ print(f"unknown session op: {op}", file=sys.stderr)
3459
+ return 2
3460
+
3461
+ # ----- run (the convenience path) -----
3462
+ if sub == "run":
3463
+ # Fast path: create-or-reuse agent + env, create session,
3464
+ # send prompt, stream until idle, print aggregated text.
3465
+ from omega_engine.managed_agent import ManagedAgentProvider
3466
+ provider = ManagedAgentProvider(
3467
+ api_key=api_key,
3468
+ agent_name=args.agent_name,
3469
+ env_name=args.env_name,
3470
+ model=args.model,
3471
+ system=args.system or "You are an Omega OS managed agent.",
3472
+ omega_home=_omega_home(),
3473
+ timeout_s=args.timeout,
3474
+ )
3475
+ # Build a minimal AgentRequest manually (no full envelope needed).
3476
+ from omega_engine.provider import AgentRequest
3477
+ req = AgentRequest(
3478
+ role=args.role, prompt=args.intent,
3479
+ system="", context={"mission_id": ""},
3480
+ )
3481
+ result = provider.run(req)
3482
+ if args.json:
3483
+ print(_json.dumps({
3484
+ "text": result.text,
3485
+ "artifacts": result.artifacts,
3486
+ }, indent=2))
3487
+ else:
3488
+ print(result.text)
3489
+ ma = result.artifacts.get("managed_agent", {})
3490
+ print()
3491
+ print(f" session: {ma.get('session_id','?')}")
3492
+ tools = ma.get("tool_uses") or []
3493
+ if tools:
3494
+ print(f" tools used: {', '.join(tools)}")
3495
+ return 0
3496
+
3497
+ print("usage: omega ma {info|agent|env|session|run} ...", file=sys.stderr)
3498
+ return 2
3499
+
3500
+
3501
+ def cmd_handoff(args: argparse.Namespace) -> int:
3502
+ """`omega handoff {write,read,list,template,from-done}` — session-end memo.
3503
+
3504
+ Five-section markdown that the next agent reads when picking up:
3505
+ 1. Mission — one sentence
3506
+ 2. State — works vs broken (concrete, runtime-observed)
3507
+ 3. Files touched — exact paths
3508
+ 4. Dead ends — what NOT to retry
3509
+ 5. Next — first concrete action
3510
+ """
3511
+ import json as _json
3512
+ from omega_engine import handoff as H
3513
+
3514
+ sub = getattr(args, "handoff_cmd", None)
3515
+ home = _omega_home()
3516
+
3517
+ if sub == "template":
3518
+ print(H.TEMPLATE)
3519
+ return 0
3520
+
3521
+ if sub == "read":
3522
+ ho = H.read_handoff(
3523
+ home,
3524
+ project=getattr(args, "project", None),
3525
+ task_id=getattr(args, "task_id", None),
3526
+ )
3527
+ if ho is None:
3528
+ target = args.project or args.task_id or "?"
3529
+ print(f"no handoff found for {target}", file=sys.stderr)
3530
+ return 1
3531
+ if getattr(args, "json", False):
3532
+ print(ho.to_json())
3533
+ else:
3534
+ print(ho.to_markdown())
3535
+ return 0
3536
+
3537
+ if sub == "list":
3538
+ rows = H.list_handoffs(home, args.project)
3539
+ if not rows:
3540
+ print(f"no archived handoffs for project '{args.project}'")
3541
+ return 0
3542
+ if getattr(args, "json", False):
3543
+ print(_json.dumps(rows, indent=2))
3544
+ return 0
3545
+ print(f"## handoff history for '{args.project}' ({len(rows)} entries)")
3546
+ for r in rows:
3547
+ print(f" {r['created_at']} — {r['mission']}")
3548
+ if r["next_first"]:
3549
+ print(f" next: {r['next_first']}")
3550
+ return 0
3551
+
3552
+ if sub == "from-done":
3553
+ draft = H.from_done_signal(home, args.task_id, cwd=args.cwd or os.getcwd())
3554
+ if args.project:
3555
+ draft.project = args.project
3556
+ # Write under task target by default; both if --project also given.
3557
+ target = "both" if args.project else "task"
3558
+ result = H.write_handoff(home, draft, target=target)
3559
+ print(f"wrote handoff:")
3560
+ for p in result["written"]:
3561
+ print(f" {p}")
3562
+ if args.print:
3563
+ print()
3564
+ print(draft.to_markdown())
3565
+ return 0
3566
+
3567
+ if sub == "write":
3568
+ # Two paths: --file <path> reads markdown to import,
3569
+ # else interactive prompt mode for the 5 fields.
3570
+ if args.file:
3571
+ text = Path(args.file).read_text()
3572
+ ho = H.Handoff.from_markdown(text)
3573
+ ho.project = args.project or ho.project
3574
+ ho.task_id = args.task_id or ho.task_id
3575
+ ho.mission_id = args.mission_id or ho.mission_id
3576
+ ho.author = args.author or ho.author or "human"
3577
+ else:
3578
+ print("interactive handoff (Ctrl-D when done with multi-line fields):")
3579
+ mission = input("Mission (one sentence): ").strip()
3580
+ print("State (multi-line, end with empty line):")
3581
+ state_lines: list[str] = []
3582
+ try:
3583
+ while True:
3584
+ line = input()
3585
+ if line == "":
3586
+ break
3587
+ state_lines.append(line)
3588
+ except EOFError:
3589
+ pass
3590
+ state = "\n".join(state_lines)
3591
+ print("Files touched (one per line, empty to end):")
3592
+ files: list[str] = []
3593
+ try:
3594
+ while True:
3595
+ line = input().strip()
3596
+ if not line:
3597
+ break
3598
+ files.append(line)
3599
+ except EOFError:
3600
+ pass
3601
+ print("Dead ends (one per line, empty to end):")
3602
+ dead: list[str] = []
3603
+ try:
3604
+ while True:
3605
+ line = input().strip()
3606
+ if not line:
3607
+ break
3608
+ dead.append(line)
3609
+ except EOFError:
3610
+ pass
3611
+ print("Next steps (one per line, empty to end):")
3612
+ nxt: list[str] = []
3613
+ try:
3614
+ while True:
3615
+ line = input().strip()
3616
+ if not line:
3617
+ break
3618
+ nxt.append(line)
3619
+ except EOFError:
3620
+ pass
3621
+ ho = H.Handoff(
3622
+ mission=mission, state=state, files_touched=files,
3623
+ dead_ends=dead, next_steps=nxt,
3624
+ author=args.author or "human",
3625
+ project=args.project or "",
3626
+ task_id=args.task_id or "",
3627
+ mission_id=args.mission_id or "",
3628
+ )
3629
+ target = "project" if args.project else ("task" if args.task_id else None)
3630
+ if target is None:
3631
+ print("error: --project or --task-id required to know where to write",
3632
+ file=sys.stderr)
3633
+ return 2
3634
+ result = H.write_handoff(home, ho, target=target)
3635
+ for p in result["written"]:
3636
+ print(f"wrote {p}")
3637
+ return 0
3638
+
3639
+ print("usage: omega handoff {write|read|list|template|from-done} ...",
3640
+ file=sys.stderr)
3641
+ return 2
3642
+
3643
+
3644
+ def cmd_worker(args: argparse.Namespace) -> int:
3645
+ """`omega worker {spawn,run,status,wait,list}` — persistent-tmux workers.
3646
+
3647
+ Each worker runs as its own `claude -p` invocation inside a detached tmux
3648
+ session. Survives parent crashes. Smart model defaults per role.
3649
+ """
3650
+ import json
3651
+ from omega_engine import worker as W
3652
+
3653
+ sub = getattr(args, "worker_cmd", None) or "list"
3654
+ home = _omega_home()
3655
+
3656
+ if sub == "spawn":
3657
+ intent = args.intent
3658
+ role = args.role
3659
+ # Resolve skills: --skills wins, else role default.
3660
+ skills_list: list[str] = []
3661
+ if getattr(args, "skills", None):
3662
+ skills_list = [s.strip() for s in args.skills.split(",") if s.strip()]
3663
+ brief = W.WorkerBrief(
3664
+ task_id=args.task_id or f"t-{int(time.time())}-{os.urandom(2).hex()}",
3665
+ role=role,
3666
+ intent=intent,
3667
+ user_prompt=args.user_prompt or intent,
3668
+ system_prompt=args.system_prompt or "",
3669
+ model=args.model or "",
3670
+ verify_cmd=args.verify_cmd,
3671
+ project=args.project,
3672
+ cwd=args.cwd or os.getcwd(),
3673
+ mission_id=args.mission_id or "",
3674
+ skills=skills_list,
3675
+ )
3676
+ name = W.spawn_worker(brief, home=home, worker_id=args.worker_id)
3677
+ print(json.dumps({
3678
+ "task_id": brief.task_id, "session": name,
3679
+ "model": brief.resolved_model(), "role": brief.role,
3680
+ "skills": brief.resolved_skills(),
3681
+ }, indent=2))
3682
+ return 0
3683
+
3684
+ if sub == "run":
3685
+ return W.run_brief(home, args.task_id)
3686
+
3687
+ if sub == "status":
3688
+ s = W.worker_status(home, args.task_id)
3689
+ print(json.dumps(s, indent=2))
3690
+ return 0 if s.get("state") in ("done_clean", "pending", "running") else 1
3691
+
3692
+ if sub == "wait":
3693
+ s = W.wait_for_done(
3694
+ home, args.task_id,
3695
+ timeout_s=args.timeout, poll_s=args.poll,
3696
+ )
3697
+ print(json.dumps(s, indent=2))
3698
+ return 0 if s.get("state") == "done_clean" else 1
3699
+
3700
+ if sub == "list":
3701
+ rows = W.list_workers(home)
3702
+ if args.json:
3703
+ print(json.dumps(rows, indent=2))
3704
+ return 0
3705
+ if not rows:
3706
+ print("no workers (Agentik_Runtime/sessions empty)")
3707
+ return 0
3708
+ print(f"{'task_id':24s} {'role':10s} {'model':28s} {'state':12s} intent")
3709
+ for r in rows:
3710
+ print(
3711
+ f"{r['task_id']:24s} {r['role']:10s} "
3712
+ f"{r['model']:28s} {r['state']:12s} {r['intent']}"
3713
+ )
3714
+ return 0
3715
+
3716
+ print(f"unknown worker subcommand: {sub}", file=sys.stderr)
3717
+ return 2
3718
+
3719
+
3720
+ def cmd_cleanup(args: argparse.Namespace) -> int:
3721
+ """`omega cleanup` — OmegaOS-adapted disk cleanup pipeline."""
3722
+ from omega_engine import cleanup as C
3723
+
3724
+ home = _omega_home()
3725
+ reports = C.cleanup(
3726
+ home,
3727
+ dry_run=not args.yes,
3728
+ deep=args.deep,
3729
+ target_pct=args.target,
3730
+ idle_days=args.idle_days,
3731
+ runtime_days=args.runtime_days,
3732
+ tmux_hours=args.tmux_hours,
3733
+ )
3734
+ if args.json:
3735
+ import json
3736
+ print(json.dumps([{
3737
+ "tier": r.tier, "items": r.items_removed,
3738
+ "bytes_freed": r.bytes_freed, "dry_run": r.dry_run,
3739
+ "detail": r.detail,
3740
+ } for r in reports], indent=2))
3741
+ else:
3742
+ print(C.format_summary(reports, Path(home)))
3743
+ return 0
3744
+
3745
+
3746
+ def _build_parser() -> argparse.ArgumentParser:
3747
+ parser = argparse.ArgumentParser(prog="omega", description="Omega OS control CLI")
3748
+ # When no subcommand is given, drop into the interactive menu instead of
3749
+ # erroring. The cmd_menu function reads the whiptail menus and dispatches.
3750
+ parser.set_defaults(fn=cmd_menu)
3751
+ sub = parser.add_subparsers(dest="cmd")
3752
+ sub.add_parser("version", help="print the engine version").set_defaults(fn=cmd_version)
3753
+ p_doc = sub.add_parser("doctor", help="validate the deployment")
3754
+ p_doc.add_argument("--json", action="store_true",
3755
+ help="emit a single JSON object instead of pretty text")
3756
+ p_doc.set_defaults(fn=cmd_doctor)
3757
+ sub.add_parser("status", help="show all tasks and their derived state").set_defaults(fn=cmd_status)
3758
+
3759
+ p_acc = sub.add_parser("account", help="manage the Claude Max account pool")
3760
+ p_acc.set_defaults(fn=cmd_account)
3761
+ acc_sub = p_acc.add_subparsers(dest="account_cmd")
3762
+ acc_sub.add_parser("list", help="show the pool (default)")
3763
+ p_login = acc_sub.add_parser("login", help="add an account via OAuth (or manual paste)")
3764
+ p_login.add_argument("--id", help="account id (e.g. max-primary)", default=None)
3765
+ p_login.add_argument("--label", help="human label", default=None)
3766
+ p_login.add_argument("--manual", action="store_true",
3767
+ help="skip the device-code attempt; paste a token directly")
3768
+ p_use = acc_sub.add_parser("use", help="set an account's status")
3769
+ p_use.add_argument("id", help="account id")
3770
+ p_use.add_argument("status", choices=["active", "resting", "disabled"])
3771
+ p_pool = acc_sub.add_parser("pool", help="edit weights and selection strategy")
3772
+ p_pool.add_argument("--selection", choices=["round-robin", "least-used", "by-quota"],
3773
+ help="set the pool-wide selection strategy")
3774
+ p_pool.add_argument("--weight", action="append", default=[],
3775
+ metavar="ID=N",
3776
+ help="set an account weight; repeatable")
3777
+
3778
+ sub.add_parser("billing", help="show usage/cost per Claude Max account").set_defaults(fn=cmd_billing)
3779
+ p_run = sub.add_parser("run", help="run a mission end-to-end")
3780
+ p_run.add_argument("intent", help="the mission, in natural language")
3781
+ p_run.set_defaults(fn=cmd_run)
3782
+ p_proj = sub.add_parser("project", help="create a project or manage its secrets")
3783
+ p_proj.set_defaults(fn=cmd_project)
3784
+ proj_sub = p_proj.add_subparsers(dest="project_cmd")
3785
+ # `omega project <name>` keeps working — the positional is bound at the
3786
+ # top level. The `secret` subcommand opens the per-project vault CLI.
3787
+ p_proj.add_argument("name", nargs="?", help="the project name (create mode)")
3788
+ p_proj.add_argument("--no-telegram", action="store_true",
3789
+ help="skip Telegram topic creation")
3790
+ p_sec = proj_sub.add_parser("secret",
3791
+ help="per-project secret vault (set|get|list)")
3792
+ p_sec.add_argument("op", choices=["set", "get", "list"])
3793
+ p_sec.add_argument("slug", help="project slug (the dir under Agentik_Coding/projects/)")
3794
+ p_sec.add_argument("--ref", help="secret reference name (for set / get)")
3795
+ p_sec.add_argument("--value", help="secret value (set; omit to prompt)")
3796
+
3797
+ # `omega classify` — task complexity classifier
3798
+ p_cls = sub.add_parser("classify", help="show how the router would classify a task")
3799
+ p_cls.add_argument("task", help="task description")
3800
+ p_cls.add_argument("--role", default="worker")
3801
+ p_cls.set_defaults(fn=cmd_classify)
3802
+
3803
+ # `omega memory` — semantic memory across all sources
3804
+ p_mem = sub.add_parser("memory", help="semantic memory across all conversations + missions")
3805
+ p_mem.set_defaults(fn=cmd_memory)
3806
+ mem_sub = p_mem.add_subparsers(dest="memory_cmd")
3807
+ mem_sub.add_parser("stats", help="show index stats")
3808
+ mem_sub.add_parser("reindex", help="rebuild the semantic index")
3809
+ p_msearch = mem_sub.add_parser("search", help="ranked search across sources")
3810
+ p_msearch.add_argument("query")
3811
+ p_msearch.add_argument("-k", type=int, default=10)
3812
+
3813
+ # `omega learn` — Smith reflection
3814
+ p_lrn = sub.add_parser("learn", help="Smith reflection on audits + worker outcomes")
3815
+ p_lrn.set_defaults(fn=cmd_learn)
3816
+ lrn_sub = p_lrn.add_subparsers(dest="learn_cmd")
3817
+ p_lref = lrn_sub.add_parser("reflect", help="run reflection + write proposals")
3818
+ p_lref.add_argument("--write", action="store_true",
3819
+ help="write proposals to staging (default: print only)")
3820
+ lrn_sub.add_parser("report", help="JSON dump of the latest reflection")
3821
+
3822
+ # `omega message` — worker ↔ oracle messaging
3823
+ p_msg = sub.add_parser("message", help="worker ↔ oracle messaging primitive")
3824
+ p_msg.set_defaults(fn=cmd_message)
3825
+ msg_sub = p_msg.add_subparsers(dest="message_cmd")
3826
+ p_msend = msg_sub.add_parser("send", help="send a message on a task")
3827
+ p_msend.add_argument("task_id")
3828
+ p_msend.add_argument("--from", dest="from_", required=True)
3829
+ p_msend.add_argument("--to", required=True)
3830
+ p_msend.add_argument("--type",
3831
+ choices=["question", "answer", "proposal", "decision"],
3832
+ required=True)
3833
+ p_msend.add_argument("--text", required=True)
3834
+ p_mread = msg_sub.add_parser("read", help="read messages on a task")
3835
+ p_mread.add_argument("task_id")
3836
+ p_mread.add_argument("--to", default=None,
3837
+ help="filter to messages addressed to this recipient")
3838
+ p_mwait = msg_sub.add_parser("wait", help="block until a message addressed to <to> arrives")
3839
+ p_mwait.add_argument("task_id")
3840
+ p_mwait.add_argument("--to", required=True)
3841
+ p_mwait.add_argument("--timeout", type=int, default=600)
3842
+ msg_sub.add_parser("open", help="list every unanswered question across all tasks")
3843
+
3844
+ # `omega tmux` — categorised tmux orchestration
3845
+ p_tx = sub.add_parser("tmux", help="tmux orchestration — list/attach/kill/spawn/menu")
3846
+ p_tx.set_defaults(fn=cmd_tmux)
3847
+ tx_sub = p_tx.add_subparsers(dest="tmux_cmd")
3848
+ tx_sub.add_parser("list", help="all sessions, grouped by category")
3849
+ tx_sub.add_parser("count", help="just the session count (for status line)")
3850
+ p_tatt = tx_sub.add_parser("attach", help="print attach command for a session")
3851
+ p_tatt.add_argument("name")
3852
+ p_tkil = tx_sub.add_parser("kill", help="kill a session")
3853
+ p_tkil.add_argument("name")
3854
+ p_tspw = tx_sub.add_parser("spawn", help="spawn a new categorised session")
3855
+ p_tspw.add_argument("kind",
3856
+ choices=["session", "oracle", "worker", "aisb-chat"])
3857
+ p_tspw.add_argument("name")
3858
+ p_tspw.add_argument("--task", default=None,
3859
+ help="task name (worker only)")
3860
+ p_tspw.add_argument("--cwd", default=None,
3861
+ help="working directory for the new session")
3862
+ tx_sub.add_parser("health", help="population summary + stale workers")
3863
+ p_tcl = tx_sub.add_parser("cleanup", help="kill stale detached worker sessions")
3864
+ p_tcl.add_argument("--hours", type=int, default=24,
3865
+ help="age threshold in hours (default 24)")
3866
+ p_tcl.add_argument("--yes", action="store_true",
3867
+ help="apply (default is dry-run)")
3868
+ tx_sub.add_parser("menu", help="interactive whiptail picker (use under tmux for prefix+Z)")
3869
+ p_tcfg = tx_sub.add_parser("config",
3870
+ help="write the recommended tmux.conf to Agentik_Tools/")
3871
+ p_tcfg.add_argument("--profile", choices=["minimal", "pro"],
3872
+ default="minimal",
3873
+ help="minimal (~30 lines) or pro (paste-fix, "
3874
+ "kill forensics, smart scroll — for 24/7 agent VPS)")
3875
+ p_tins = tx_sub.add_parser(
3876
+ "install",
3877
+ help="install the OmegaOS tmux.conf into ~/.tmux.conf "
3878
+ "(with timestamped backup of the existing one)",
3879
+ )
3880
+ p_tins.add_argument("--profile", choices=["minimal", "pro"],
3881
+ default="pro",
3882
+ help="default 'pro' — paste-fix, kill forensics, "
3883
+ "smart scroll. Use 'minimal' for the lighter version.")
3884
+ p_tins.add_argument("--no-backup", action="store_true",
3885
+ help="skip the backup of the existing ~/.tmux.conf "
3886
+ "(NOT recommended)")
3887
+
3888
+ # `omega worker` — persistent-tmux workers (smart model per role)
3889
+ p_wk = sub.add_parser(
3890
+ "worker",
3891
+ help="spawn / run / inspect persistent-tmux workers (smart model per role)",
3892
+ )
3893
+ p_wk.set_defaults(fn=cmd_worker)
3894
+ wk_sub = p_wk.add_subparsers(dest="worker_cmd")
3895
+ # spawn
3896
+ p_wsp = wk_sub.add_parser(
3897
+ "spawn",
3898
+ help="write a brief + dispatch a detached tmux session running `omega worker run`",
3899
+ )
3900
+ p_wsp.add_argument("intent", help="natural-language task")
3901
+ p_wsp.add_argument("--role", default="worker",
3902
+ help="provider role (default 'worker' → opus 1M context)")
3903
+ p_wsp.add_argument("--task-id", default=None,
3904
+ help="explicit task id (default: auto-generated t-...)")
3905
+ p_wsp.add_argument("--model", default="",
3906
+ help="override the role default (e.g. 'sonnet', 'opus', 'haiku')")
3907
+ p_wsp.add_argument("--system-prompt", default="",
3908
+ help="envelope system prompt (--append-system-prompt)")
3909
+ p_wsp.add_argument("--user-prompt", default="",
3910
+ help="explicit user prompt (default: same as intent)")
3911
+ p_wsp.add_argument("--verify-cmd", default=None,
3912
+ help="shell command that proves completion")
3913
+ p_wsp.add_argument("--project", default=None,
3914
+ help="project slug (used in tmux session name)")
3915
+ p_wsp.add_argument("--cwd", default=None,
3916
+ help="working directory for the worker (default: $PWD)")
3917
+ p_wsp.add_argument("--mission-id", default="",
3918
+ help="parent mission id (for traceability)")
3919
+ p_wsp.add_argument("--worker-id", type=int, default=1,
3920
+ help="worker # within the mission (default 1)")
3921
+ p_wsp.add_argument("--skills", default=None,
3922
+ help="comma-separated skill ids (overrides role default; "
3923
+ "e.g. 'pursue,codeaudit')")
3924
+ # run
3925
+ p_wrn = wk_sub.add_parser(
3926
+ "run",
3927
+ help="entry point inside the spawned tmux session — reads brief.json + runs claude",
3928
+ )
3929
+ p_wrn.add_argument("task_id", help="task id whose brief.json to execute")
3930
+ # status
3931
+ p_wst = wk_sub.add_parser("status", help="show the worker's state (.done.json + tmux)")
3932
+ p_wst.add_argument("task_id", help="task id")
3933
+ # wait
3934
+ p_wwt = wk_sub.add_parser("wait", help="block until .done.json appears (or timeout)")
3935
+ p_wwt.add_argument("task_id", help="task id")
3936
+ p_wwt.add_argument("--timeout", type=int, default=3600,
3937
+ help="max seconds to wait (default 3600)")
3938
+ p_wwt.add_argument("--poll", type=int, default=5,
3939
+ help="poll interval in seconds (default 5)")
3940
+ # list
3941
+ p_wls = wk_sub.add_parser("list", help="every worker on disk (Agentik_Runtime/sessions)")
3942
+ p_wls.add_argument("--json", action="store_true", help="emit JSON instead of a table")
3943
+
3944
+ # `omega hermes` — Layer-2 Hermes companion (Nous Research, OAuth bridge)
3945
+ p_he = sub.add_parser(
3946
+ "hermes",
3947
+ help="Hermes (Nous Research) Layer-2 companion — uses the SAME "
3948
+ "Claude Max OAuth as `claude`. install / link / status / ask.",
3949
+ )
3950
+ p_he.set_defaults(fn=cmd_hermes)
3951
+ he_sub = p_he.add_subparsers(dest="hermes_cmd")
3952
+ p_hest = he_sub.add_parser("status",
3953
+ help="installed + linked + OAuth token + expiry snapshot")
3954
+ p_hest.add_argument("--json", action="store_true")
3955
+ p_hein = he_sub.add_parser("install",
3956
+ help="run upstream install.sh (idempotent; --yes to re-install)")
3957
+ p_hein.add_argument("--yes", action="store_true",
3958
+ help="re-install even if hermes is already on PATH")
3959
+ p_hein.add_argument("--with-browser", action="store_true",
3960
+ help="also install Playwright/Chromium (~150 MB)")
3961
+ p_hein.add_argument("--dry-run", action="store_true",
3962
+ help="print the command without running it")
3963
+ p_hein.add_argument("--timeout", type=int, default=900,
3964
+ help="install timeout in seconds (default 900)")
3965
+ p_hein.add_argument("--json", action="store_true")
3966
+ p_hel = he_sub.add_parser("link",
3967
+ help="wire Hermes to use the live Claude OAuth token")
3968
+ p_hel.add_argument("--provider", default="anthropic",
3969
+ help="Hermes provider id (default: anthropic)")
3970
+ p_hel.add_argument("--model", default="claude-opus-4-7",
3971
+ help="Hermes default model (default: claude-opus-4-7)")
3972
+ p_hel.add_argument("--dry-run", action="store_true")
3973
+ p_hel.add_argument("--json", action="store_true")
3974
+ p_heask = he_sub.add_parser("ask",
3975
+ help="one-shot prompt → hermes → stdout (uses Claude OAuth)")
3976
+ p_heask.add_argument("prompt", help="natural-language prompt")
3977
+ p_heask.add_argument("--timeout", type=int, default=600)
3978
+ he_sub.add_parser("env",
3979
+ help="print the env vars the bridge would inject (debug helper)")
3980
+ p_hebr = he_sub.add_parser(
3981
+ "brief",
3982
+ help="teach Hermès what OmegaOS is + record outcome observations "
3983
+ "for the self-improvement loop",
3984
+ )
3985
+ p_hebr.add_argument("brief_op", nargs="?", default="status",
3986
+ choices=["status", "write", "observe"],
3987
+ help="status (default) | write | observe")
3988
+ p_hebr.add_argument("slug", nargs="?", default=None,
3989
+ help="(observe) project slug")
3990
+ p_hebr.add_argument("task_id", nargs="?", default=None,
3991
+ help="(observe) task id whose .done.json to ingest")
3992
+
3993
+ # `omega hermes-desktop` — Hermes Desktop remote backend
3994
+ p_hd = sub.add_parser(
3995
+ "hermes-desktop",
3996
+ help="Expose this VPS as a remote backend for the Hermes Desktop "
3997
+ "Electron app (https://github.com/fathah/hermes-desktop).",
3998
+ )
3999
+ p_hd.set_defaults(fn=cmd_hermes_desktop)
4000
+ hd_sub = p_hd.add_subparsers(dest="hd_cmd")
4001
+ p_hdi = hd_sub.add_parser("install",
4002
+ help="generate the token + print the desktop profile to paste")
4003
+ p_hdi.add_argument("--port", type=int, default=8642)
4004
+ p_hdi.add_argument("--public-url", default=None,
4005
+ help="public URL the desktop will connect to "
4006
+ "(default: http://127.0.0.1:<port>)")
4007
+ p_hdp = hd_sub.add_parser("profile",
4008
+ help="print the desktop profile as JSON")
4009
+ p_hdp.add_argument("--port", type=int, default=8642)
4010
+ p_hdp.add_argument("--public-url", default=None)
4011
+ p_hds = hd_sub.add_parser("status",
4012
+ help="snapshot — token present + port free?")
4013
+ p_hds.add_argument("--port", type=int, default=8642)
4014
+ p_hdsv = hd_sub.add_parser("serve",
4015
+ help="bind + serve the HTTP adapter (blocking — run inside tmux)")
4016
+ p_hdsv.add_argument("--port", type=int, default=8642)
4017
+ p_hdsv.add_argument("--unsafe-public", action="store_true",
4018
+ help="bind 0.0.0.0 (NOT recommended; use SSH tunnel)")
4019
+
4020
+ # `omega ua` — Understand-Anything (Claude Code plugin) bridge
4021
+ p_ua = sub.add_parser(
4022
+ "ua",
4023
+ help="Understand-Anything plugin bridge — install + /understand "
4024
+ "+ consume the knowledge graph into GraphRetriever",
4025
+ )
4026
+ p_ua.set_defaults(fn=cmd_ua)
4027
+ ua_sub = p_ua.add_subparsers(dest="ua_cmd")
4028
+ p_uain = ua_sub.add_parser("install",
4029
+ help="install the Claude Code plugin (uses Max OAuth)")
4030
+ p_uain.add_argument("--timeout", type=int, default=120)
4031
+ p_uain.add_argument("--dry-run", action="store_true")
4032
+ p_uain.add_argument("--json", action="store_true")
4033
+ p_uar = ua_sub.add_parser("run",
4034
+ help="trigger /understand on a project (produces "
4035
+ ".understand-anything/knowledge-graph.json)")
4036
+ p_uar.add_argument("project", help="project directory")
4037
+ p_uar.add_argument("--language", default=None,
4038
+ help="output language (e.g. fr, ja)")
4039
+ p_uar.add_argument("--timeout", type=int, default=1800)
4040
+ p_uar.add_argument("--json", action="store_true")
4041
+ p_uac = ua_sub.add_parser("consume",
4042
+ help="merge the produced graph into OmegaOS's GraphRetriever")
4043
+ p_uac.add_argument("project", help="project directory containing the output")
4044
+
4045
+ # `omega ma` — Anthropic Managed Agents (opt-in, API key required)
4046
+ p_ma = sub.add_parser(
4047
+ "ma",
4048
+ help="Anthropic Managed Agents lifecycle "
4049
+ "(opt-in; requires ANTHROPIC_API_KEY). Complement to claude-max, "
4050
+ "NOT a replacement.",
4051
+ )
4052
+ p_ma.set_defaults(fn=cmd_ma)
4053
+ ma_sub = p_ma.add_subparsers(dest="ma_cmd")
4054
+ ma_sub.add_parser("info",
4055
+ help="show API-key status + cached agent/env IDs + docs link")
4056
+ # agent
4057
+ p_maag = ma_sub.add_parser("agent", help="manage Managed-Agents Agents (templates)")
4058
+ maag_sub = p_maag.add_subparsers(dest="agent_op")
4059
+ p_maagc = maag_sub.add_parser("create", help="create + cache a new agent")
4060
+ p_maagc.add_argument("name", help="local cache name (e.g. omega-default-agent)")
4061
+ p_maagc.add_argument("--model", default="claude-opus-4-7")
4062
+ p_maagc.add_argument("--system", default="",
4063
+ help="system prompt baked into the agent template")
4064
+ p_maagg = maag_sub.add_parser("get", help="get an agent by id")
4065
+ p_maagg.add_argument("id")
4066
+ p_maagl = maag_sub.add_parser("list", help="list agents from the org")
4067
+ p_maagl.add_argument("--limit", type=int, default=20)
4068
+ p_maagl.add_argument("--json", action="store_true")
4069
+ p_maagd = maag_sub.add_parser("delete", help="delete an agent by id")
4070
+ p_maagd.add_argument("id")
4071
+ # env
4072
+ p_maen = ma_sub.add_parser("env", help="manage Managed-Agents Environments (sandboxes)")
4073
+ maen_sub = p_maen.add_subparsers(dest="env_op")
4074
+ p_maenc = maen_sub.add_parser("create", help="create + cache a new environment (cloud)")
4075
+ p_maenc.add_argument("name", help="local cache name")
4076
+ p_maeng = maen_sub.add_parser("get", help="get an environment by id")
4077
+ p_maeng.add_argument("id")
4078
+ p_maenl = maen_sub.add_parser("list", help="list environments from the org")
4079
+ p_maenl.add_argument("--limit", type=int, default=20)
4080
+ p_maenl.add_argument("--json", action="store_true")
4081
+ p_maend = maen_sub.add_parser("delete", help="delete an environment by id")
4082
+ p_maend.add_argument("id")
4083
+ # session
4084
+ p_mase = ma_sub.add_parser("session", help="manage Managed-Agents Sessions (running instances)")
4085
+ mase_sub = p_mase.add_subparsers(dest="session_op")
4086
+ p_masec = mase_sub.add_parser("create", help="create a session")
4087
+ p_masec.add_argument("--agent", required=True,
4088
+ help="agent id OR cache name")
4089
+ p_masec.add_argument("--env", required=True,
4090
+ help="environment id OR cache name")
4091
+ p_masec.add_argument("--title", default=None)
4092
+ p_masese = mase_sub.add_parser("send", help="send a user message into a session")
4093
+ p_masese.add_argument("id", help="session id")
4094
+ p_masese.add_argument("text", help="user message text")
4095
+ p_masest = mase_sub.add_parser("stream", help="stream SSE events from a session")
4096
+ p_masest.add_argument("id", help="session id")
4097
+ p_masest.add_argument("--timeout", type=int, default=600)
4098
+ p_masel = mase_sub.add_parser("list")
4099
+ p_masel.add_argument("--limit", type=int, default=20)
4100
+ p_masel.add_argument("--json", action="store_true")
4101
+ p_maseg = mase_sub.add_parser("get")
4102
+ p_maseg.add_argument("id")
4103
+ p_masedd = mase_sub.add_parser("delete")
4104
+ p_masedd.add_argument("id")
4105
+ # run (convenience)
4106
+ p_mar = ma_sub.add_parser(
4107
+ "run",
4108
+ help="convenience: create-or-reuse agent + env, start session, "
4109
+ "send the intent, stream until idle, return the aggregated text",
4110
+ )
4111
+ p_mar.add_argument("intent", help="natural-language task")
4112
+ p_mar.add_argument("--agent-name", default="omega-default-agent",
4113
+ help="cached agent name (created on first use)")
4114
+ p_mar.add_argument("--env-name", default="omega-default-env",
4115
+ help="cached environment name (created on first use)")
4116
+ p_mar.add_argument("--model", default="claude-opus-4-7")
4117
+ p_mar.add_argument("--system", default="",
4118
+ help="system prompt for the agent (if creating)")
4119
+ p_mar.add_argument("--role", default="worker",
4120
+ help="OmegaOS role tag (informational only)")
4121
+ p_mar.add_argument("--timeout", type=int, default=600)
4122
+ p_mar.add_argument("--json", action="store_true")
4123
+
4124
+ # `omega genesis` — programmatic project genesis pipeline
4125
+ p_gen = sub.add_parser(
4126
+ "genesis",
4127
+ help="Project genesis: vision → market (5) → branding (3) → "
4128
+ "PRD (9) → features (N with depends_on) → handoff to plan. "
4129
+ "Strict per-project isolation.",
4130
+ )
4131
+ p_gen.set_defaults(fn=cmd_genesis)
4132
+ gen_sub = p_gen.add_subparsers(dest="genesis_cmd")
4133
+ p_gnew = gen_sub.add_parser("new", help="initialise a new project")
4134
+ p_gnew.add_argument("slug", help="project slug (kebab-case, no spaces)")
4135
+ p_gnew.add_argument("--name", default=None, help="display name")
4136
+ p_gnew.add_argument("--intent", default=None,
4137
+ help="one-line intent (operator brief)")
4138
+ p_gnew.add_argument("--accept-defaults", action="store_true",
4139
+ help="use the OmegaOS canonical stack without prompts")
4140
+ p_gnew.add_argument("--force", action="store_true",
4141
+ help="overwrite PROJECT.yaml if it already exists")
4142
+ p_gnew.add_argument("--json", action="store_true")
4143
+ for k, opts in (
4144
+ ("type", ["web", "mobile", "desktop", "iphone", "mac", "cli",
4145
+ "library", "mixed"]),
4146
+ ("frontend", ["nextjs", "astro", "remix", "vite-react", "expo",
4147
+ "tauri", "electron", "swiftui"]),
4148
+ ("backend", ["convex", "supabase", "postgres+drizzle",
4149
+ "trpc+prisma", "firebase", "none"]),
4150
+ ("deploy", ["vercel", "cloudflare", "fly", "render",
4151
+ "self-hosted", "aws-amplify", "none", "eas"]),
4152
+ ("payments", ["stripe", "lemonsqueezy", "paddle", "none"]),
4153
+ ("ui_kit", ["shadcn", "radix", "mantine", "mui",
4154
+ "tailwind-only", "custom"]),
4155
+ ("auth", ["clerk", "better-auth", "next-auth",
4156
+ "supabase-auth", "custom"]),
4157
+ ):
4158
+ p_gnew.add_argument(f"--stack-{k.replace('_','-')}", dest=f"stack_{k}",
4159
+ choices=opts, default=None,
4160
+ help=f"override stack.{k}")
4161
+ p_gquest = gen_sub.add_parser(
4162
+ "questions",
4163
+ help="print the stack-picker question list (for the bot / skill UI)",
4164
+ )
4165
+ p_gquest.add_argument("--intent", default=None)
4166
+ p_gquest.add_argument("--json", action="store_true")
4167
+ p_gstat = gen_sub.add_parser("status", help="phase + completed + stack snapshot")
4168
+ p_gstat.add_argument("slug")
4169
+ p_gstat.add_argument("--json", action="store_true")
4170
+ p_grun = gen_sub.add_parser(
4171
+ "run",
4172
+ help="advance the pipeline (default: to completion; --phase to run one)",
4173
+ )
4174
+ p_grun.add_argument("slug")
4175
+ p_grun.add_argument("--phase", default=None,
4176
+ help="run only this phase (vision/market/branding/prd/features/plan)")
4177
+ p_grun.add_argument("--max", type=int, default=None,
4178
+ help="cap the number of phases this call advances")
4179
+ p_gdel = gen_sub.add_parser("delete",
4180
+ help="destroy the project tree (requires --yes)")
4181
+ p_gdel.add_argument("slug")
4182
+ p_gdel.add_argument("--yes", action="store_true")
4183
+
4184
+ # `omega plan` — FSM-enforced sequential plan executor
4185
+ p_pl = sub.add_parser(
4186
+ "plan",
4187
+ help="Plan v7 — FSM-enforced sequential plan executor (fixes the "
4188
+ "v6 task-skipping bug by construction, not by prompt).",
4189
+ )
4190
+ p_pl.set_defaults(fn=cmd_plan)
4191
+ pl_sub = p_pl.add_subparsers(dest="plan_cmd")
4192
+ p_pb = pl_sub.add_parser("build",
4193
+ help="load plan/DAG.json into the per-project store")
4194
+ p_pb.add_argument("slug")
4195
+ p_pb.add_argument("--reset-failed", action="store_true",
4196
+ help="re-set every FAILED task to PENDING")
4197
+ p_pn = pl_sub.add_parser("next",
4198
+ help="show the next eligible task (deps all VERIFIED)")
4199
+ p_pn.add_argument("slug")
4200
+ p_pn.add_argument("--json", action="store_true")
4201
+ p_ps = pl_sub.add_parser("status",
4202
+ help="completion %% + per-state counts + active task")
4203
+ p_ps.add_argument("slug")
4204
+ p_ps.add_argument("--json", action="store_true")
4205
+ p_pl_ls = pl_sub.add_parser("list", help="every task (filter with --state)")
4206
+ p_pl_ls.add_argument("slug")
4207
+ p_pl_ls.add_argument("--state",
4208
+ choices=["pending", "in_progress", "claimed_done",
4209
+ "verified", "failed"], default=None)
4210
+ p_pl_ls.add_argument("--json", action="store_true")
4211
+ p_pst = pl_sub.add_parser("start",
4212
+ help="PENDING → IN_PROGRESS (requires deps VERIFIED)")
4213
+ p_pst.add_argument("slug"); p_pst.add_argument("task_id")
4214
+ p_pcd = pl_sub.add_parser("claimed",
4215
+ help="IN_PROGRESS → CLAIMED_DONE (verify next)")
4216
+ p_pcd.add_argument("slug"); p_pcd.add_argument("task_id")
4217
+ p_pdn = pl_sub.add_parser(
4218
+ "done",
4219
+ help="CLAIMED_DONE → VERIFIED via verify_cmd (rejects on non-zero)",
4220
+ )
4221
+ p_pdn.add_argument("slug"); p_pdn.add_argument("task_id")
4222
+ p_pdn.add_argument("--no-verify", action="store_true",
4223
+ help="skip the verify_cmd (operator override)")
4224
+ p_pfa = pl_sub.add_parser("fail",
4225
+ help="force-fail a task (operator action)")
4226
+ p_pfa.add_argument("slug"); p_pfa.add_argument("task_id")
4227
+ p_pfa.add_argument("--reason", default=None)
4228
+ p_prt = pl_sub.add_parser("retry",
4229
+ help="FAILED → PENDING (operator action)")
4230
+ p_prt.add_argument("slug"); p_prt.add_argument("task_id")
4231
+ p_prn = pl_sub.add_parser(
4232
+ "run",
4233
+ help="autonomous multi-day loop: next → in_progress → dispatch "
4234
+ "→ claimed → verify → verified (FSM-enforced; reducer rejects "
4235
+ "any task whose deps aren't VERIFIED).",
4236
+ )
4237
+ p_prn.add_argument("slug")
4238
+ p_prn.add_argument("--max", type=int, default=None,
4239
+ help="stop after N iterations (default: until no eligible task)")
4240
+ p_prn.add_argument("--spawn-cmd", default=None,
4241
+ help="shell command to dispatch each task; supports "
4242
+ "{task_id}/{slug}/{title}/{verify}/{skill}/{files} substitution")
4243
+ p_prn.add_argument("--sleep", type=float, default=0.0,
4244
+ help="seconds to sleep between iterations (rate limit)")
4245
+ p_prn.add_argument("--json", action="store_true")
4246
+
4247
+ # `omega handoff` — 5-section session handoff memo
4248
+ p_ho = sub.add_parser(
4249
+ "handoff",
4250
+ help="5-section session handoff memo "
4251
+ "(mission / state / files / dead-ends / next)",
4252
+ )
4253
+ p_ho.set_defaults(fn=cmd_handoff)
4254
+ ho_sub = p_ho.add_subparsers(dest="handoff_cmd")
4255
+ ho_sub.add_parser("template",
4256
+ help="print the canonical 5-section template")
4257
+ p_horead = ho_sub.add_parser("read",
4258
+ help="print the latest handoff for a project or task")
4259
+ p_horead.add_argument("--project", help="project slug")
4260
+ p_horead.add_argument("--task-id", help="task id")
4261
+ p_horead.add_argument("--json", action="store_true",
4262
+ help="emit JSON instead of markdown")
4263
+ p_holist = ho_sub.add_parser("list",
4264
+ help="chronological list of archived handoffs for a project")
4265
+ p_holist.add_argument("project", help="project slug")
4266
+ p_holist.add_argument("--json", action="store_true",
4267
+ help="emit JSON instead of pretty text")
4268
+ p_howrite = ho_sub.add_parser("write",
4269
+ help="write a handoff (interactive prompts unless --file is given)")
4270
+ p_howrite.add_argument("--project", default=None, help="project slug")
4271
+ p_howrite.add_argument("--task-id", default=None, help="task id")
4272
+ p_howrite.add_argument("--mission-id", default=None, help="mission id")
4273
+ p_howrite.add_argument("--author", default=None,
4274
+ help="who wrote this (default: 'human')")
4275
+ p_howrite.add_argument("--file", default=None,
4276
+ help="markdown file to import (skips prompts)")
4277
+ p_hofd = ho_sub.add_parser("from-done",
4278
+ help="auto-draft a handoff from a worker's .done.json + git diff")
4279
+ p_hofd.add_argument("task_id", help="task id whose .done.json to read")
4280
+ p_hofd.add_argument("--project", default=None,
4281
+ help="also write under the project (slug)")
4282
+ p_hofd.add_argument("--cwd", default=None,
4283
+ help="git repo for `git diff --name-only` (default: $PWD)")
4284
+ p_hofd.add_argument("--print", action="store_true",
4285
+ help="also print the rendered markdown after writing")
4286
+
4287
+ # `omega cleanup` — OmegaOS-adapted disk cleanup
4288
+ p_cln = sub.add_parser(
4289
+ "cleanup",
4290
+ help="OmegaOS-adapted disk cleanup (tiers 1-6 + optional --deep). "
4291
+ "Default is dry-run; pass --yes to actually free space.",
4292
+ )
4293
+ p_cln.add_argument("--yes", action="store_true",
4294
+ help="actually delete (default is dry-run)")
4295
+ p_cln.add_argument("--deep", action="store_true",
4296
+ help="also run the deep tiers (staging, transcripts, VACUUM, opensrc, git gc)")
4297
+ p_cln.add_argument("--target", type=int, default=None,
4298
+ help="stop early once disk usage drops below TARGET%%")
4299
+ p_cln.add_argument("--idle-days", type=int, default=7,
4300
+ help="node_modules pruning idle threshold (default 7d)")
4301
+ p_cln.add_argument("--runtime-days", type=int, default=30,
4302
+ help="runtime DB / sessions retention (default 30d)")
4303
+ p_cln.add_argument("--tmux-hours", type=int, default=24,
4304
+ help="stale tmux worker threshold (default 24h)")
4305
+ p_cln.add_argument("--json", action="store_true",
4306
+ help="emit per-tier JSON instead of pretty text")
4307
+ p_cln.set_defaults(fn=cmd_cleanup)
4308
+
4309
+ # `omega aisb` — Telegram-fallback chat with the AISB master agent
4310
+ p_ai = sub.add_parser("aisb", help="talk to AISB outside Telegram (tmux fallback)")
4311
+ p_ai.set_defaults(fn=cmd_aisb)
4312
+ ai_sub = p_ai.add_subparsers(dest="aisb_cmd")
4313
+ ai_sub.add_parser("chat", help="spawn (or attach to) the AISB-chat tmux session")
4314
+ ai_sub.add_parser("chat-loop", help="the REPL itself (called by the tmux session)")
4315
+
4316
+ # `omega validate providers` — 1-token API key validation
4317
+ p_val = sub.add_parser("validate", help="validate provider credentials with a 1-token round-trip")
4318
+ p_val.set_defaults(fn=cmd_validate)
4319
+ val_sub = p_val.add_subparsers(dest="validate_cmd")
4320
+ p_vprov = val_sub.add_parser("providers", help="validate every wired provider")
4321
+ p_vprov.add_argument("--provider", default=None,
4322
+ help="validate only this provider id (claude|glm|openai|deepseek)")
4323
+
4324
+ # `omega completions` — write + install shell completion scripts
4325
+ p_comp = sub.add_parser("completions", help="generate + install shell completion scripts")
4326
+ p_comp.set_defaults(fn=cmd_completions)
4327
+ comp_sub = p_comp.add_subparsers(dest="completions_cmd")
4328
+ comp_sub.add_parser("write", help="(re)write completion scripts under Agentik_Tools/completions/")
4329
+ p_cinst = comp_sub.add_parser("install", help="symlink the completion into the operator's shell")
4330
+ p_cinst.add_argument("shell", choices=["bash", "zsh", "fish"])
4331
+
4332
+ # `omega update {check,apply,banner,auto}` — full auto-update lifecycle
4333
+ p_upd = sub.add_parser(
4334
+ "update",
4335
+ help="auto-update lifecycle: check / apply / banner / auto",
4336
+ )
4337
+ p_upd.add_argument("--check", action="store_true",
4338
+ help="(legacy) same as `omega update check`")
4339
+ p_upd.set_defaults(fn=cmd_update)
4340
+ upd_sub = p_upd.add_subparsers(dest="update_cmd")
4341
+ upd_sub.add_parser("check",
4342
+ help="print current + latest from npm (no install)")
4343
+ p_uapp = upd_sub.add_parser(
4344
+ "apply",
4345
+ help="backup → npm install → migrate → upgrade → verify; rollback on fail",
4346
+ )
4347
+ p_uapp.add_argument("--no-backup", action="store_true",
4348
+ help="skip the pre-update backup (NOT recommended)")
4349
+ p_uapp.add_argument("--no-verify", action="store_true",
4350
+ help="skip the post-update doctor + smoke")
4351
+ p_uapp.add_argument("--no-npm", action="store_true",
4352
+ help="don't run `npm install` (assume already on disk)")
4353
+ p_uapp.add_argument("--json", action="store_true",
4354
+ help="emit JSON instead of pretty text")
4355
+ upd_sub.add_parser(
4356
+ "banner",
4357
+ help="emit the one-line outdated banner to stderr "
4358
+ "(invoked by every omega CLI startup; opt-out via OMEGA_NO_UPDATE_BANNER=1)",
4359
+ )
4360
+ p_uauto = upd_sub.add_parser(
4361
+ "auto",
4362
+ help="enable / disable / inspect the daily auto-apply charter",
4363
+ )
4364
+ p_uauto.add_argument("auto_op", nargs="?", default="status",
4365
+ choices=["status", "enable", "disable"],
4366
+ help="status (default) / enable / disable")
4367
+
4368
+ # `omega smoke` — synthetic mission
4369
+ p_smk = sub.add_parser("smoke", help="synthetic mission post-install verification")
4370
+ p_smk.set_defaults(fn=cmd_smoke)
4371
+
4372
+ # `omega prune` — bound SQLite growth
4373
+ p_pr = sub.add_parser("prune", help="prune old rows from events / audits / telegram / sessions")
4374
+ p_pr.add_argument("target", choices=["events", "audits", "telegram", "sessions", "all"])
4375
+ p_pr.add_argument("--days", type=int, default=90,
4376
+ help="prune rows older than DAYS (default 90)")
4377
+ p_pr.add_argument("--yes", action="store_true",
4378
+ help="apply (default is dry-run)")
4379
+ p_pr.set_defaults(fn=cmd_prune)
4380
+
4381
+ # `omega backup` — tarball + optional age-encrypt
4382
+ p_bk = sub.add_parser("backup", help="backup + restore + verify the recoverable subset of $OMEGA_HOME")
4383
+ p_bk.set_defaults(fn=cmd_backup)
4384
+ bk_sub = p_bk.add_subparsers(dest="backup_cmd")
4385
+ p_bcr = bk_sub.add_parser("create", help="create a backup tarball")
4386
+ p_bcr.add_argument("--out", default=None, help="output directory (default ~/)")
4387
+ p_bcr.add_argument("--encrypt", action="store_true",
4388
+ help="age-encrypt the tarball (recipient = .vault-pub)")
4389
+ p_bcr.add_argument("--recipient", default=None,
4390
+ help="path to age recipient public key (default .vault-pub)")
4391
+ p_brst = bk_sub.add_parser("restore", help="restore a backup tarball")
4392
+ p_brst.add_argument("bundle", help="path to the backup tarball")
4393
+ p_brst.add_argument("--target", default=None,
4394
+ help="destination tree (default $OMEGA_HOME)")
4395
+ p_brst.add_argument("--identity", default=None,
4396
+ help="age identity file (default .vault-key)")
4397
+ p_bv = bk_sub.add_parser("verify", help="extract the bundle into a temp dir + check shape")
4398
+ p_bv.add_argument("bundle", help="path to the backup tarball")
4399
+ p_bv.add_argument("--identity", default=None)
4400
+
4401
+ # `omega costs` — per-account USD projections
4402
+ p_co = sub.add_parser("costs", help="project per-account usage into USD via the built-in rate card")
4403
+ p_co.set_defaults(fn=cmd_costs)
4404
+
4405
+ # `omega mission rerun <id>`
4406
+ p_mi = sub.add_parser("mission", help="manage past missions (rerun, ...)")
4407
+ p_mi.set_defaults(fn=cmd_mission)
4408
+ mi_sub = p_mi.add_subparsers(dest="mission_cmd")
4409
+ p_mrer = mi_sub.add_parser("rerun", help="re-dispatch a past mission with fresh state")
4410
+ p_mrer.add_argument("id", help="mission id (m-...)")
4411
+
4412
+ # `omega project precommit install <slug>`
4413
+ p_pc = sub.add_parser("precommit", help="install / uninstall pre-commit hook in a project")
4414
+ p_pc.set_defaults(fn=cmd_precommit)
4415
+ pc_sub = p_pc.add_subparsers(dest="precommit_cmd")
4416
+ p_pcin = pc_sub.add_parser("install", help="write a pre-commit hook running codeaudit")
4417
+ p_pcin.add_argument("slug", help="project slug")
4418
+ p_pcun = pc_sub.add_parser("uninstall", help="remove the pre-commit hook (restore backup if any)")
4419
+ p_pcun.add_argument("slug", help="project slug")
4420
+
4421
+ # `omega webhook` — inbound webhook registry + test gate
4422
+ p_wh = sub.add_parser(
4423
+ "webhook",
4424
+ help="inbound webhooks (GitHub/Linear/generic) — list, show, test",
4425
+ )
4426
+ p_wh.set_defaults(fn=cmd_webhook)
4427
+ wh_sub = p_wh.add_subparsers(dest="webhook_cmd")
4428
+ wh_sub.add_parser("list", help="show every configured webhook")
4429
+ p_whshow = wh_sub.add_parser("show", help="full spec for one webhook")
4430
+ p_whshow.add_argument("id")
4431
+ p_whtest = wh_sub.add_parser("test", help="fire a synthetic signed POST through the gate")
4432
+ p_whtest.add_argument("id")
4433
+ p_whtest.add_argument("--body", default=None,
4434
+ help="raw JSON body (default: {\"intent\":\"synthetic test\"})")
4435
+
4436
+ # `omega uninstall` — clean removal of the deployment
4437
+ p_uns = sub.add_parser(
4438
+ "uninstall",
4439
+ help="cleanly remove OmegaOS from this machine "
4440
+ "(strips settings.json keys, removes services, optionally LLM CLIs)",
4441
+ )
4442
+ p_uns.add_argument("--yes", action="store_true",
4443
+ help="skip the confirmation prompt")
4444
+ p_uns.add_argument("--purge", action="store_true",
4445
+ help="also delete the encrypted vault "
4446
+ "(default: back it up to ~/.omega-vault-backup-*)")
4447
+ p_uns.add_argument("--include-llm-clis", action="store_true",
4448
+ help="also npm/uv uninstall the LLM CLIs")
4449
+ p_uns.set_defaults(fn=cmd_uninstall)
4450
+
4451
+ # `omega upgrade` — re-run the installer in place
4452
+ p_upg = sub.add_parser(
4453
+ "upgrade",
4454
+ help="re-run the installer (preserves the runtime tree); pulls "
4455
+ "in new install steps",
4456
+ )
4457
+ p_upg.add_argument("--profile", default=None,
4458
+ choices=["vps", "workstation", "minimal"])
4459
+ p_upg.add_argument("--manifest", default=None)
4460
+ p_upg.add_argument("--non-interactive", action="store_true")
4461
+ p_upg.set_defaults(fn=cmd_upgrade)
4462
+
4463
+ # `omega skill` — discover + audit + install skills (safe-by-default)
4464
+ p_skl = sub.add_parser("skill", help="discover, audit, install Claude Code skills")
4465
+ p_skl.set_defaults(fn=cmd_skill)
4466
+ skl_sub = p_skl.add_subparsers(dest="skill_cmd")
4467
+ skl_sub.add_parser("marketplaces", help="show catalog of marketplaces")
4468
+ p_sfind = skl_sub.add_parser("find", help="search the curated catalog")
4469
+ p_sfind.add_argument("query", nargs="?", default=None)
4470
+ p_sfind.add_argument("--min-trust", default="low",
4471
+ choices=["low", "medium", "high"])
4472
+ p_sfind.add_argument("--recommended", action="store_true",
4473
+ help="only the curated picks")
4474
+ p_saud = skl_sub.add_parser("audit", help="safety-audit a SKILL.md")
4475
+ p_saud.add_argument("path", help="path to SKILL.md")
4476
+ p_sins = skl_sub.add_parser("install", help="install a skill from a GitHub source")
4477
+ p_sins.add_argument("name", help="local name for the skill (becomes the dir)")
4478
+ p_sins.add_argument("--from", dest="from_", required=True,
4479
+ help="source — github:<org>/<repo>[/<subpath>]")
4480
+ p_sins.add_argument("--allow-blocked", action="store_true",
4481
+ help="install even if the auditor blocked it (NOT recommended)")
4482
+ p_srec = skl_sub.add_parser(
4483
+ "recommend",
4484
+ help="show the curated skill palette for a role "
4485
+ "(used by `omega worker spawn`)",
4486
+ )
4487
+ p_srec.add_argument("role",
4488
+ help="role id (worker, oracle, audit, classifier, ...)")
4489
+ skl_sub.add_parser(
4490
+ "catalog",
4491
+ help="every skill present under $OMEGA_HOME/Agentik_SSOT/skills/",
4492
+ )
4493
+ skl_sub.add_parser(
4494
+ "link",
4495
+ help="symlink projected SSOT skills into ~/.claude/skills/ so they "
4496
+ "become real slash commands (idempotent; run after `omega sync`)",
4497
+ )
4498
+ p_swf = skl_sub.add_parser(
4499
+ "workflows",
4500
+ help="show canonical skill-chain workflows (dispatch→pursue→audit→fix→re-audit, …)",
4501
+ )
4502
+ p_swf.add_argument("name", nargs="?", default=None,
4503
+ help="workflow id (omitted → list all)")
4504
+
4505
+ # `omega agent` — inspect resolved agent prompts (debugging)
4506
+ p_ag = sub.add_parser("agent", help="list or inspect agent prompts")
4507
+ p_ag.set_defaults(fn=cmd_agent)
4508
+ ag_sub = p_ag.add_subparsers(dest="agent_cmd")
4509
+ ag_sub.add_parser("list", help="every role under Agentik_SSOT/agents/")
4510
+ p_apr = ag_sub.add_parser("prompt", help="print the resolved system + user prompt")
4511
+ p_apr.add_argument("role", help="role name (oracle, worker, architect, ...)")
4512
+ p_apr.add_argument("--intent", default=None,
4513
+ help="example intent to embed in the user message (optional)")
4514
+
4515
+ # `omega audit` — the Quality Arsenal CLI (run, all, history, batch, gen)
4516
+ p_aud = sub.add_parser("audit", help="run a forensic audit or browse history")
4517
+ p_aud.set_defaults(fn=cmd_audit)
4518
+ aud_sub = p_aud.add_subparsers(dest="audit_cmd")
4519
+ p_arun = aud_sub.add_parser("run", help="run one audit (gather + analyse)")
4520
+ p_arun.add_argument("audit_id", help="audit id (e.g. codeaudit)")
4521
+ p_arun.add_argument("--scope", default=None, help="path to audit")
4522
+ p_arun.add_argument("--role", default="worker")
4523
+ p_arun.add_argument("--fix", action="store_true",
4524
+ help="also batch + dispatch fix workers + re-audit")
4525
+ p_arun.add_argument("--max-workers", type=int, default=3,
4526
+ help="cap fix-worker parallelism (default 3)")
4527
+ p_arun.add_argument("--min-severity", default=None,
4528
+ choices=["low", "medium", "high", "critical"],
4529
+ help="ignore findings below this severity in --fix mode")
4530
+ p_aall = aud_sub.add_parser("all", help="run every applicable audit (read-only by default)")
4531
+ p_aall.add_argument("--scope", default=None)
4532
+ p_aall.add_argument("--role", default="worker")
4533
+ p_aall.add_argument("--fix", action="store_true")
4534
+ p_aall.add_argument("--max-workers", type=int, default=3)
4535
+ p_aall.add_argument("--min-severity", default=None,
4536
+ choices=["low", "medium", "high", "critical"])
4537
+ p_ahist = aud_sub.add_parser("history", help="past runs for an audit")
4538
+ p_ahist.add_argument("audit_id", nargs="?", default=None)
4539
+ p_ahist.add_argument("--limit", type=int, default=20)
4540
+ p_abatch = aud_sub.add_parser("batch",
4541
+ help="show what batches the last run would form")
4542
+ p_abatch.add_argument("audit_id")
4543
+ p_abatch.add_argument("--max-workers", type=int, default=3)
4544
+ p_abatch.add_argument("--min-severity", default=None,
4545
+ choices=["low", "medium", "high", "critical"])
4546
+ aud_sub.add_parser("gen", help="regenerate every SKILL.md from the YAMLs")
4547
+ aud_sub.add_parser("gate",
4548
+ help="Stop-hook gatekeeper: reads JSON on stdin, "
4549
+ "denies the Stop if the latest .done.json or audit "
4550
+ "verdict says not done")
4551
+ p_adiff = aud_sub.add_parser("diff", help="diff findings between two audit runs")
4552
+ p_adiff.add_argument("run1", help="older run id")
4553
+ p_adiff.add_argument("run2", help="newer run id")
4554
+ p_adiff.set_defaults(fn=cmd_audit_diff)
4555
+
4556
+ # `omega pursue` — goal-driven loop (Agentik OS native /goal replacement)
4557
+ p_pursue = sub.add_parser(
4558
+ "pursue",
4559
+ help="loop Read/Edit/Bash until a verify_cmd exits 0 "
4560
+ "(Agentik OS goal-driven worker)",
4561
+ )
4562
+ p_pursue.add_argument("verify_cmd", help="shell command that proves the goal is met (exit 0)")
4563
+ p_pursue.add_argument("intent", help="natural-language goal")
4564
+ p_pursue.add_argument("--max-iters", type=int, default=20,
4565
+ help="hard cap on iterations (default 20)")
4566
+ p_pursue.add_argument("--project", default=None,
4567
+ help="project slug — cwd defaults to that project's dir")
4568
+ p_pursue.add_argument("--role", default="worker", help="provider role (default 'worker')")
4569
+ p_pursue.add_argument("--cwd", default=None,
4570
+ help="working directory for verify_cmd")
4571
+ p_pursue.set_defaults(fn=cmd_pursue)
4572
+
4573
+ # `omega cadence` — scheduled recurring execution (Agentik OS native /loop)
4574
+ p_cad = sub.add_parser(
4575
+ "cadence",
4576
+ help="schedule a recurring beat via the AutonomousSupervisor "
4577
+ "(Agentik OS /loop equivalent)",
4578
+ )
4579
+ p_cad.set_defaults(fn=cmd_cadence)
4580
+ cad_sub = p_cad.add_subparsers(dest="cadence_cmd")
4581
+ p_csched = cad_sub.add_parser("schedule", help="schedule a new cadence")
4582
+ p_csched.add_argument("trigger", choices=["cron", "delay", "self"])
4583
+ p_csched.add_argument("schedule",
4584
+ help="cron expr, delay-seconds, or literal 'self'")
4585
+ p_csched.add_argument("intent",
4586
+ help="what to do at each beat (natural language)")
4587
+ cad_sub.add_parser("list", help="show every scheduled cadence")
4588
+ p_cdis = cad_sub.add_parser("disable", help="set a cadence to enabled=false")
4589
+ p_cdis.add_argument("id", help="charter id (cadence-...)")
4590
+ p_crm = cad_sub.add_parser("remove", help="delete a cadence charter")
4591
+ p_crm.add_argument("id", help="charter id (cadence-...)")
4592
+
4593
+ p_dae = sub.add_parser("daemon", help="run a 24/7 service daemon")
4594
+ p_dae.add_argument("name", choices=["engine", "telegram", "autonomous"],
4595
+ help="which daemon to run")
4596
+ p_dae.set_defaults(fn=cmd_daemon)
4597
+
4598
+ p_tool = sub.add_parser("tool", help="manage installed tools (incl. MCP servers)")
4599
+ p_tool.set_defaults(fn=cmd_tool)
4600
+ tool_sub = p_tool.add_subparsers(dest="tool_cmd")
4601
+ tool_sub.add_parser("list", help="list installed tools (default)")
4602
+ p_tinst = tool_sub.add_parser("install", help="install from the MCP catalog (default) or via --npm")
4603
+ p_tinst.add_argument("name", help="tool name (an MCP catalog id, or a free name when --npm is given)")
4604
+ p_tinst.add_argument("--npm", default=None, metavar="PACKAGE",
4605
+ help="install a bare npm package under Agentik_Tools/<name>/")
4606
+ p_trm = tool_sub.add_parser("remove", help="remove a tool + unregister")
4607
+ p_trm.add_argument("name", help="tool name")
4608
+ p_tup = tool_sub.add_parser("update", help="re-install a tool from its recorded source")
4609
+ p_tup.add_argument("name", help="tool name")
4610
+
4611
+ sub.add_parser(
4612
+ "sync",
4613
+ help="project Agentik_SSOT/ into each provider's native shape",
4614
+ ).set_defaults(fn=cmd_sync)
4615
+
4616
+ p_graph = sub.add_parser(
4617
+ "graph",
4618
+ help="consume a graphify-built graph.json into Graph RAG "
4619
+ "(graphify itself is a Claude Code skill)",
4620
+ )
4621
+ p_graph.add_argument(
4622
+ "path",
4623
+ help="repo / directory containing graphify-out/graph.json, "
4624
+ "OR the literal word 'install' to register the /graphify skill",
4625
+ )
4626
+ p_graph.add_argument(
4627
+ "--json", default=None,
4628
+ help="override the graph.json path (default: <path>/graphify-out/graph.json)",
4629
+ )
4630
+ p_graph.add_argument(
4631
+ "--rag", action="store_true",
4632
+ help="merge nodes + edges into the engine's GraphRetriever "
4633
+ "(Agentik_Runtime/memory/graph.json)",
4634
+ )
4635
+ p_graph.add_argument(
4636
+ "--platform", default="claude",
4637
+ help="when path=install, which Claude-compatible platform to target "
4638
+ "(default: claude)",
4639
+ )
4640
+ p_graph.set_defaults(fn=cmd_graph)
4641
+ return parser
4642
+
4643
+
4644
+ def main(argv: list[str] | None = None) -> int:
4645
+ parser = _build_parser()
4646
+ args = parser.parse_args(argv)
4647
+ # Auto-update banner: fast cached check (no network unless stale,
4648
+ # < 5 ms). Skipped for the `update` and `doctor --json` commands so
4649
+ # neither auto-update nor machine-readable output get polluted by it.
4650
+ cmd = getattr(args, "cmd", None)
4651
+ skip_banner = (
4652
+ cmd in ("update", "completions", "version")
4653
+ or (cmd == "doctor" and getattr(args, "json", False))
4654
+ )
4655
+ if not skip_banner:
4656
+ try:
4657
+ from omega_engine.auto_update import maybe_emit_banner
4658
+ maybe_emit_banner(_omega_home())
4659
+ except Exception: # noqa: BLE001
4660
+ # Banner failure must NEVER block the actual command.
4661
+ pass
154
4662
  return args.fn(args)
155
4663
 
156
4664