@agentikos/omega-os 0.19.52 → 0.19.54

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.
@@ -676,6 +676,30 @@ except Exception: print("unknown")' 2>/dev/null)" || readiness="unknown"
676
676
  printf ' %s %sissues%s %sgithub.com/agentik-os/OmegaOS/issues%s\n' "$bar" "$C_BOLD" "$C_RST" "$C_DIM" "$C_RST"
677
677
  printf ' %s\n' "$bot"
678
678
 
679
+ # ── Terminal config tips for Option+key shortcuts ────────────────────
680
+ # tmux binds M-z / M-/ for the Omega popup menu + session switcher.
681
+ # macOS Terminal.app + Termius default send the literal Unicode
682
+ # character on Option+key (Ω, ÷) instead of the Esc+key (Meta) sequence
683
+ # tmux expects. The Unicode fallback binds in tmux.py cover that, but
684
+ # configuring the terminal once gives the user a proper Meta key.
685
+ log ""
686
+ log "${C_ORANGE}━ Terminal config required for Option+key shortcuts ━${C_RST}"
687
+ log ""
688
+ log " ${C_BOLD}Option+Z${C_RST} → Omega menu (popup)"
689
+ log " ${C_BOLD}Option+/${C_RST} → Session switcher (popup)"
690
+ log " ${C_BOLD}Option+O${C_RST} → Omega menu (alternate, when tmux-claude is preserved)"
691
+ log ""
692
+ log " For these to fire, your terminal must send Option as Esc+ (Meta):"
693
+ log ""
694
+ log " ${C_BOLD}iTerm2${C_RST} Settings → Profiles → Keys → Left Option Key: ${C_BOLD}Esc+${C_RST}"
695
+ log " ${C_BOLD}Terminal.app${C_RST} Settings → Profile → Keyboard → ☑ ${C_BOLD}Use Option as Meta key${C_RST}"
696
+ log " ${C_BOLD}Termius${C_RST} Settings → Terminal → Option key → ${C_BOLD}Send Esc${C_RST}"
697
+ log " ${C_BOLD}Warp${C_RST} Settings → Input → ☑ ${C_BOLD}Option as Meta${C_RST}"
698
+ log " ${C_BOLD}Alacritty${C_RST} add to alacritty.toml: ${C_DIM}[env]\\nALACRITTY_OPTION_AS_ALT = \"Both\"${C_RST}"
699
+ log ""
700
+ log " Without this, Option+Z sends ${C_DIM}Ω${C_RST} (Unicode) instead of ${C_DIM}\\e z${C_RST} — tmux never sees the bind."
701
+ log ""
702
+
679
703
  # If PARTIAL or NOT READY, tell them where to look.
680
704
  case "$verdict_text" in
681
705
  PARTIAL|"NOT READY")
@@ -427,6 +427,15 @@ step_claude_code_settings() {
427
427
  }
428
428
 
429
429
  step_engine() {
430
+ # v0.19.54 — neutralize broken claude-mem here too, NOT just in
431
+ # step_claude_plugins. The plugins step can be skipped/idempotent on
432
+ # re-install, but step_engine ALWAYS re-runs on version drift (it's
433
+ # in the force-rerun list). This guarantees the broken plugin is
434
+ # disabled on every upgrade, even if step 45 is somehow skipped.
435
+ if declare -f _disable_broken_claude_plugins >/dev/null 2>&1; then
436
+ _disable_broken_claude_plugins
437
+ fi
438
+
430
439
  local uv_bin; uv_bin="$(command -v uv || echo "$HOME/.local/bin/uv")"
431
440
  [ -x "$uv_bin" ] || { err "uv not found after step 10"; return 1; }
432
441
  cd "$OMEGA_HOME/Agentik_Engine" || { err "engine block missing"; return 1; }
@@ -3,192 +3,256 @@
3
3
  Event-sourced, verified-completion agent graphs. The engine is business-agnostic:
4
4
  it executes graphs of Tasks. Who is an "oracle" and which topology runs lives in
5
5
  Agentik_Orchestration/, never here.
6
+
7
+ v0.19.53 — PEP 562 lazy attribute loading. Previously, importing
8
+ ``omega_engine`` (or any submodule like ``omega_engine.cli``) eagerly pulled
9
+ the full submodule tree — ``account``, ``autonomous``, ``audit``, ``educators``,
10
+ ``rag``, ``skill_discovery``, ``genesis``, ``provider``, ``executor`` and 40+
11
+ others — at module-load time. On macOS uv-Python that's ~500-1500ms of import
12
+ cost paid before *any* CLI argument is parsed, which made bare ``omega``
13
+ (menu) feel sluggish even after v0.19.50 removed runtime tmux probes.
14
+
15
+ Each public re-export is now declared in ``_LAZY_MAP``: a mapping from the
16
+ public name to ``(submodule_path, attribute_name)``. ``__getattr__`` (PEP
17
+ 562) resolves the import on first access, so ``from omega_engine import
18
+ WorkerBrief`` still works, but ``import omega_engine`` itself touches *zero*
19
+ heavy submodules.
20
+
21
+ Submodules (``omega_engine.tmux``, ``omega_engine.cli``, etc.) are still
22
+ auto-importable by Python's standard submodule mechanism — they don't need
23
+ to be in ``_LAZY_MAP``.
6
24
  """
7
- from omega_engine.account import (
8
- AccountPool,
9
- BillingAggregator,
10
- ClaudeAccount,
11
- )
12
- from omega_engine.audit import AuditFinding, AuditGate, AuditVerdict
13
- from omega_engine.audit_arsenal import (
14
- ArsenalGate,
15
- ArsenalVerdict,
16
- Audit,
17
- AuditRegistry,
18
- run_forensic_audit,
19
- )
20
- from omega_engine.autonomous import (
21
- AutonomousSupervisor,
22
- Charter,
23
- build_supervisor_from_home,
24
- load_charters,
25
- next_fire,
26
- parse_cron,
27
- )
28
- from omega_engine.barrier import ScopeStatus, scope_status
29
- from omega_engine.educators import (
30
- Artifact,
31
- ArtifactEducator,
32
- AutomationEducator,
33
- ClaudecodeEducator,
34
- ConnectionEducator,
35
- CoworkerEducator,
36
- Educator,
37
- EducatorProposal,
38
- EducatorRegistry,
39
- LoopEducator,
40
- PromptEducator,
41
- SkillEducator,
42
- StagingPipeline,
43
- StagingRecord,
44
- )
45
- from omega_engine.bus import EventBus
46
- from omega_engine.events import Event, EventType
47
- from omega_engine.executor import Executor, MissionResult
48
- from omega_engine.integrations import (
49
- GraphifyError,
50
- GraphifyGraphMissing,
51
- GraphifyNotInstalled,
52
- GraphifyReport,
53
- graphify_install_skill,
54
- graphify_into_rag,
55
- graphify_load_graph,
56
- graphify_version,
57
- is_graphify_installed,
58
- )
59
- from omega_engine.progress import MissionProgress, ProgressTracker
60
- from omega_engine.provider import (
61
- AgentProvider,
62
- AgentRequest,
63
- AgentResult,
64
- ClaudeProvider,
65
- DeepSeekProvider,
66
- GLMProvider,
67
- MockProvider,
68
- OpenAIProvider,
69
- )
70
- from omega_engine.rag import (
71
- AgenticRetriever,
72
- CorrectiveRetriever,
73
- Document,
74
- GraphRetriever,
75
- HybridRetriever,
76
- MultimodalRetriever,
77
- RAGRouter,
78
- RetrievalResult,
79
- Retriever,
80
- )
81
- from omega_engine.reducer import IllegalTransition, reduce, reduce_task
82
- from omega_engine.router import ModelRouter
83
- from omega_engine.store import EventStore, SQLiteStore
84
- from omega_engine.cadence import (
85
- CadenceCharter,
86
- disable_cadence,
87
- list_cadences,
88
- remove_cadence,
89
- schedule_cadence,
90
- )
91
- from omega_engine.pursue import PursueResult, pursue
92
- from omega_engine.webhooks import (
93
- WebhookRegistry,
94
- WebhookSpec,
95
- generate_test_signature,
96
- handle_webhook,
97
- load_registry as load_webhook_registry,
98
- signature_header_for,
99
- verify_signature,
100
- )
101
- from omega_engine.skill_discovery import (
102
- DiscoveredSkill,
103
- Marketplace,
104
- SkillAuditIssue,
105
- SkillAuditVerdict,
106
- SkillInstallResult,
107
- audit_skill_file,
108
- audit_skill_text,
109
- discover_skills,
110
- install_skill_from_github,
111
- list_curated,
112
- load_marketplaces,
113
- )
114
- from omega_engine.sync import ClaudeCodeAdapter, SyncEngine
115
- from omega_engine.done_signal import (
116
- DoneSignal,
117
- DoneSignalError,
118
- attach_audit_to_done,
119
- done_path,
120
- read_done,
121
- session_dir,
122
- write_done,
123
- )
124
- from omega_engine.envelope import (
125
- DISPATCHER_ROLES,
126
- EXECUTOR_ROLES,
127
- EnvelopeContext,
128
- VERIFIER_ROLES,
129
- build_envelope,
130
- )
131
- from omega_engine.prompts import (
132
- find_agent_file,
133
- list_available_agents,
134
- load_agent_prompt,
135
- )
136
- from omega_engine.vault import (
137
- list_project_vaults,
138
- vault_init,
139
- vault_read,
140
- vault_status,
141
- vault_write,
142
- )
143
- from omega_engine.task import (
144
- TERMINAL,
145
- Budget,
146
- Kind,
147
- Lifecycle,
148
- Task,
149
- TaskState,
150
- Trigger,
151
- )
152
- from omega_engine.tools import Tool, ToolRegistry, install_from_catalog
153
- from omega_engine.worker import (
154
- WorkerBrief,
155
- default_model_for_role,
156
- list_workers,
157
- read_brief,
158
- run_brief,
159
- spawn_worker,
160
- wait_for_done,
161
- worker_status,
162
- write_brief,
163
- )
164
- from omega_engine.cleanup import (
165
- TierResult,
166
- cleanup as run_cleanup,
167
- format_summary as cleanup_summary,
168
- )
169
- from omega_engine.skill_routing import (
170
- POWER_SKILLS,
171
- catalog_for_home as skill_catalog_for_home,
172
- format_skill_block,
173
- recommend as recommend_skills,
174
- skills_for,
175
- )
176
- from omega_engine.genesis import (
177
- GenesisOrchestrator,
178
- GenesisState,
179
- PhaseResult,
180
- Stack,
181
- StackConfig,
182
- StackQuestion,
183
- init_project as genesis_init_project,
184
- load_state as genesis_load_state,
185
- pick_stack,
186
- project_root as genesis_project_root,
187
- stack_questions,
188
- )
189
- from omega_engine import plan as plan_v7
25
+ from __future__ import annotations
26
+
27
+ import importlib
28
+ from typing import Any
29
+
30
+ __version__ = "0.19.54"
31
+
32
+ # Public-name → (module_path, attribute_name) map.
33
+ # ``attribute_name = None`` means "return the whole module" (used for the
34
+ # ``plan_v7`` alias that points at the ``omega_engine.plan`` submodule).
35
+ _LAZY_MAP: dict[str, tuple[str, str | None]] = {
36
+ # account pool + billing
37
+ "AccountPool": ("omega_engine.account", "AccountPool"),
38
+ "BillingAggregator": ("omega_engine.account", "BillingAggregator"),
39
+ "ClaudeAccount": ("omega_engine.account", "ClaudeAccount"),
40
+ # audit (the canonical AuditGate / AuditVerdict / AuditFinding)
41
+ "AuditFinding": ("omega_engine.audit", "AuditFinding"),
42
+ "AuditGate": ("omega_engine.audit", "AuditGate"),
43
+ "AuditVerdict": ("omega_engine.audit", "AuditVerdict"),
44
+ # audit_arsenal — multi-audit forensic harness
45
+ "ArsenalGate": ("omega_engine.audit_arsenal", "ArsenalGate"),
46
+ "ArsenalVerdict": ("omega_engine.audit_arsenal", "ArsenalVerdict"),
47
+ "Audit": ("omega_engine.audit_arsenal", "Audit"),
48
+ "AuditRegistry": ("omega_engine.audit_arsenal", "AuditRegistry"),
49
+ "run_forensic_audit": ("omega_engine.audit_arsenal", "run_forensic_audit"),
50
+ # autonomous-agent supervisor + charters
51
+ "AutonomousSupervisor": ("omega_engine.autonomous", "AutonomousSupervisor"),
52
+ "Charter": ("omega_engine.autonomous", "Charter"),
53
+ "build_supervisor_from_home": ("omega_engine.autonomous", "build_supervisor_from_home"),
54
+ "load_charters": ("omega_engine.autonomous", "load_charters"),
55
+ "next_fire": ("omega_engine.autonomous", "next_fire"),
56
+ "parse_cron": ("omega_engine.autonomous", "parse_cron"),
57
+ # barrier
58
+ "ScopeStatus": ("omega_engine.barrier", "ScopeStatus"),
59
+ "scope_status": ("omega_engine.barrier", "scope_status"),
60
+ # educators — the self-improving layer
61
+ "Artifact": ("omega_engine.educators", "Artifact"),
62
+ "ArtifactEducator": ("omega_engine.educators", "ArtifactEducator"),
63
+ "AutomationEducator": ("omega_engine.educators", "AutomationEducator"),
64
+ "ClaudecodeEducator": ("omega_engine.educators", "ClaudecodeEducator"),
65
+ "ConnectionEducator": ("omega_engine.educators", "ConnectionEducator"),
66
+ "CoworkerEducator": ("omega_engine.educators", "CoworkerEducator"),
67
+ "Educator": ("omega_engine.educators", "Educator"),
68
+ "EducatorProposal": ("omega_engine.educators", "EducatorProposal"),
69
+ "EducatorRegistry": ("omega_engine.educators", "EducatorRegistry"),
70
+ "LoopEducator": ("omega_engine.educators", "LoopEducator"),
71
+ "PromptEducator": ("omega_engine.educators", "PromptEducator"),
72
+ "SkillEducator": ("omega_engine.educators", "SkillEducator"),
73
+ "StagingPipeline": ("omega_engine.educators", "StagingPipeline"),
74
+ "StagingRecord": ("omega_engine.educators", "StagingRecord"),
75
+ # bus
76
+ "EventBus": ("omega_engine.bus", "EventBus"),
77
+ # events
78
+ "Event": ("omega_engine.events", "Event"),
79
+ "EventType": ("omega_engine.events", "EventType"),
80
+ # executor
81
+ "Executor": ("omega_engine.executor", "Executor"),
82
+ "MissionResult": ("omega_engine.executor", "MissionResult"),
83
+ # integrations — graphify
84
+ "GraphifyError": ("omega_engine.integrations", "GraphifyError"),
85
+ "GraphifyGraphMissing": ("omega_engine.integrations", "GraphifyGraphMissing"),
86
+ "GraphifyNotInstalled": ("omega_engine.integrations", "GraphifyNotInstalled"),
87
+ "GraphifyReport": ("omega_engine.integrations", "GraphifyReport"),
88
+ "graphify_install_skill": ("omega_engine.integrations", "graphify_install_skill"),
89
+ "graphify_into_rag": ("omega_engine.integrations", "graphify_into_rag"),
90
+ "graphify_load_graph": ("omega_engine.integrations", "graphify_load_graph"),
91
+ "graphify_version": ("omega_engine.integrations", "graphify_version"),
92
+ "is_graphify_installed": ("omega_engine.integrations", "is_graphify_installed"),
93
+ # progress
94
+ "MissionProgress": ("omega_engine.progress", "MissionProgress"),
95
+ "ProgressTracker": ("omega_engine.progress", "ProgressTracker"),
96
+ # provider
97
+ "AgentProvider": ("omega_engine.provider", "AgentProvider"),
98
+ "AgentRequest": ("omega_engine.provider", "AgentRequest"),
99
+ "AgentResult": ("omega_engine.provider", "AgentResult"),
100
+ "ClaudeProvider": ("omega_engine.provider", "ClaudeProvider"),
101
+ "DeepSeekProvider": ("omega_engine.provider", "DeepSeekProvider"),
102
+ "GLMProvider": ("omega_engine.provider", "GLMProvider"),
103
+ "MockProvider": ("omega_engine.provider", "MockProvider"),
104
+ "OpenAIProvider": ("omega_engine.provider", "OpenAIProvider"),
105
+ # rag
106
+ "AgenticRetriever": ("omega_engine.rag", "AgenticRetriever"),
107
+ "CorrectiveRetriever": ("omega_engine.rag", "CorrectiveRetriever"),
108
+ "Document": ("omega_engine.rag", "Document"),
109
+ "GraphRetriever": ("omega_engine.rag", "GraphRetriever"),
110
+ "HybridRetriever": ("omega_engine.rag", "HybridRetriever"),
111
+ "MultimodalRetriever": ("omega_engine.rag", "MultimodalRetriever"),
112
+ "RAGRouter": ("omega_engine.rag", "RAGRouter"),
113
+ "RetrievalResult": ("omega_engine.rag", "RetrievalResult"),
114
+ "Retriever": ("omega_engine.rag", "Retriever"),
115
+ # reducer
116
+ "IllegalTransition": ("omega_engine.reducer", "IllegalTransition"),
117
+ "reduce": ("omega_engine.reducer", "reduce"),
118
+ "reduce_task": ("omega_engine.reducer", "reduce_task"),
119
+ # router
120
+ "ModelRouter": ("omega_engine.router", "ModelRouter"),
121
+ # store
122
+ "EventStore": ("omega_engine.store", "EventStore"),
123
+ "SQLiteStore": ("omega_engine.store", "SQLiteStore"),
124
+ # cadence
125
+ "CadenceCharter": ("omega_engine.cadence", "CadenceCharter"),
126
+ "disable_cadence": ("omega_engine.cadence", "disable_cadence"),
127
+ "list_cadences": ("omega_engine.cadence", "list_cadences"),
128
+ "remove_cadence": ("omega_engine.cadence", "remove_cadence"),
129
+ "schedule_cadence": ("omega_engine.cadence", "schedule_cadence"),
130
+ # pursue
131
+ "PursueResult": ("omega_engine.pursue", "PursueResult"),
132
+ "pursue": ("omega_engine.pursue", "pursue"),
133
+ # webhooks
134
+ "WebhookRegistry": ("omega_engine.webhooks", "WebhookRegistry"),
135
+ "WebhookSpec": ("omega_engine.webhooks", "WebhookSpec"),
136
+ "generate_test_signature": ("omega_engine.webhooks", "generate_test_signature"),
137
+ "handle_webhook": ("omega_engine.webhooks", "handle_webhook"),
138
+ "load_webhook_registry": ("omega_engine.webhooks", "load_registry"),
139
+ "signature_header_for": ("omega_engine.webhooks", "signature_header_for"),
140
+ "verify_signature": ("omega_engine.webhooks", "verify_signature"),
141
+ # skill_discovery
142
+ "DiscoveredSkill": ("omega_engine.skill_discovery", "DiscoveredSkill"),
143
+ "Marketplace": ("omega_engine.skill_discovery", "Marketplace"),
144
+ "SkillAuditIssue": ("omega_engine.skill_discovery", "SkillAuditIssue"),
145
+ "SkillAuditVerdict": ("omega_engine.skill_discovery", "SkillAuditVerdict"),
146
+ "SkillInstallResult": ("omega_engine.skill_discovery", "SkillInstallResult"),
147
+ "audit_skill_file": ("omega_engine.skill_discovery", "audit_skill_file"),
148
+ "audit_skill_text": ("omega_engine.skill_discovery", "audit_skill_text"),
149
+ "discover_skills": ("omega_engine.skill_discovery", "discover_skills"),
150
+ "install_skill_from_github": ("omega_engine.skill_discovery", "install_skill_from_github"),
151
+ "list_curated": ("omega_engine.skill_discovery", "list_curated"),
152
+ "load_marketplaces": ("omega_engine.skill_discovery", "load_marketplaces"),
153
+ # sync
154
+ "ClaudeCodeAdapter": ("omega_engine.sync", "ClaudeCodeAdapter"),
155
+ "SyncEngine": ("omega_engine.sync", "SyncEngine"),
156
+ # done_signal
157
+ "DoneSignal": ("omega_engine.done_signal", "DoneSignal"),
158
+ "DoneSignalError": ("omega_engine.done_signal", "DoneSignalError"),
159
+ "attach_audit_to_done": ("omega_engine.done_signal", "attach_audit_to_done"),
160
+ "done_path": ("omega_engine.done_signal", "done_path"),
161
+ "read_done": ("omega_engine.done_signal", "read_done"),
162
+ "session_dir": ("omega_engine.done_signal", "session_dir"),
163
+ "write_done": ("omega_engine.done_signal", "write_done"),
164
+ # envelope
165
+ "DISPATCHER_ROLES": ("omega_engine.envelope", "DISPATCHER_ROLES"),
166
+ "EXECUTOR_ROLES": ("omega_engine.envelope", "EXECUTOR_ROLES"),
167
+ "EnvelopeContext": ("omega_engine.envelope", "EnvelopeContext"),
168
+ "VERIFIER_ROLES": ("omega_engine.envelope", "VERIFIER_ROLES"),
169
+ "build_envelope": ("omega_engine.envelope", "build_envelope"),
170
+ # prompts
171
+ "find_agent_file": ("omega_engine.prompts", "find_agent_file"),
172
+ "list_available_agents": ("omega_engine.prompts", "list_available_agents"),
173
+ "load_agent_prompt": ("omega_engine.prompts", "load_agent_prompt"),
174
+ # vault
175
+ "list_project_vaults": ("omega_engine.vault", "list_project_vaults"),
176
+ "vault_init": ("omega_engine.vault", "vault_init"),
177
+ "vault_read": ("omega_engine.vault", "vault_read"),
178
+ "vault_status": ("omega_engine.vault", "vault_status"),
179
+ "vault_write": ("omega_engine.vault", "vault_write"),
180
+ # task model + FSM
181
+ "TERMINAL": ("omega_engine.task", "TERMINAL"),
182
+ "Budget": ("omega_engine.task", "Budget"),
183
+ "Kind": ("omega_engine.task", "Kind"),
184
+ "Lifecycle": ("omega_engine.task", "Lifecycle"),
185
+ "Task": ("omega_engine.task", "Task"),
186
+ "TaskState": ("omega_engine.task", "TaskState"),
187
+ "Trigger": ("omega_engine.task", "Trigger"),
188
+ # tools
189
+ "Tool": ("omega_engine.tools", "Tool"),
190
+ "ToolRegistry": ("omega_engine.tools", "ToolRegistry"),
191
+ "install_from_catalog": ("omega_engine.tools", "install_from_catalog"),
192
+ # worker — persistent tmux worker briefs
193
+ "WorkerBrief": ("omega_engine.worker", "WorkerBrief"),
194
+ "default_model_for_role": ("omega_engine.worker", "default_model_for_role"),
195
+ "list_workers": ("omega_engine.worker", "list_workers"),
196
+ "read_brief": ("omega_engine.worker", "read_brief"),
197
+ "run_brief": ("omega_engine.worker", "run_brief"),
198
+ "spawn_worker": ("omega_engine.worker", "spawn_worker"),
199
+ "wait_for_done": ("omega_engine.worker", "wait_for_done"),
200
+ "worker_status": ("omega_engine.worker", "worker_status"),
201
+ "write_brief": ("omega_engine.worker", "write_brief"),
202
+ # cleanup
203
+ "TierResult": ("omega_engine.cleanup", "TierResult"),
204
+ "run_cleanup": ("omega_engine.cleanup", "cleanup"),
205
+ "cleanup_summary": ("omega_engine.cleanup", "format_summary"),
206
+ # skill_routing
207
+ "POWER_SKILLS": ("omega_engine.skill_routing", "POWER_SKILLS"),
208
+ "skill_catalog_for_home": ("omega_engine.skill_routing", "catalog_for_home"),
209
+ "format_skill_block": ("omega_engine.skill_routing", "format_skill_block"),
210
+ "recommend_skills": ("omega_engine.skill_routing", "recommend"),
211
+ "skills_for": ("omega_engine.skill_routing", "skills_for"),
212
+ # genesis
213
+ "GenesisOrchestrator": ("omega_engine.genesis", "GenesisOrchestrator"),
214
+ "GenesisState": ("omega_engine.genesis", "GenesisState"),
215
+ "PhaseResult": ("omega_engine.genesis", "PhaseResult"),
216
+ "Stack": ("omega_engine.genesis", "Stack"),
217
+ "StackConfig": ("omega_engine.genesis", "StackConfig"),
218
+ "StackQuestion": ("omega_engine.genesis", "StackQuestion"),
219
+ "genesis_init_project": ("omega_engine.genesis", "init_project"),
220
+ "genesis_load_state": ("omega_engine.genesis", "load_state"),
221
+ "pick_stack": ("omega_engine.genesis", "pick_stack"),
222
+ "genesis_project_root": ("omega_engine.genesis", "project_root"),
223
+ "stack_questions": ("omega_engine.genesis", "stack_questions"),
224
+ # plan submodule re-export (was: `from omega_engine import plan as plan_v7`)
225
+ "plan_v7": ("omega_engine.plan", None),
226
+ }
227
+
228
+
229
+ def __getattr__(name: str) -> Any:
230
+ """PEP 562 lazy attribute loader.
231
+
232
+ Resolves the import on first access, caches the value on this module so
233
+ subsequent lookups bypass __getattr__. Submodules (``omega_engine.tmux``
234
+ etc.) are NOT in _LAZY_MAP; Python's standard submodule import handles
235
+ those automatically when they're referenced as ``omega_engine.<name>`` or
236
+ ``from omega_engine import <submodule>``.
237
+ """
238
+ entry = _LAZY_MAP.get(name)
239
+ if entry is None:
240
+ raise AttributeError(
241
+ f"module 'omega_engine' has no attribute {name!r}"
242
+ )
243
+ mod_path, attr = entry
244
+ module = importlib.import_module(mod_path)
245
+ value = module if attr is None else getattr(module, attr)
246
+ # Cache so subsequent lookups are direct dict reads.
247
+ globals()[name] = value
248
+ return value
249
+
250
+
251
+ def __dir__() -> list[str]:
252
+ """Make tab-completion and introspection show all public names without
253
+ triggering a load."""
254
+ return sorted(list(globals().keys()) + list(_LAZY_MAP.keys()))
190
255
 
191
- __version__ = "0.19.52"
192
256
 
193
257
  __all__ = [
194
258
  "__version__",
@@ -2949,11 +2949,28 @@ def _attach_or_spawn_chat(session_name: str, *,
2949
2949
  print(f" could not spawn {label}: {exc}")
2950
2950
  print(f" try manually: tmux new -d -s {session_name}")
2951
2951
  return 2
2952
- # Inside tmux already? Tell the user to switch (we can't from here).
2952
+ # Inside tmux already? Do the switch ourselves v0.19.54 fix.
2953
+ # Previously we PRINTED instructions ("Switch with: tmux switch-client
2954
+ # -t Omega:hermes") and exited, leaving the user staring at a useless
2955
+ # hint. Same bug pattern as cmd_menu had in v0.19.40 (fixed v0.19.41).
2956
+ # tmux switch-client works fine from inside a tmux client and is
2957
+ # exactly what the user wants.
2953
2958
  if os.environ.get("TMUX"):
2954
- print(f" already inside tmux — switch with:")
2955
- print(f" tmux switch-client -t {session_name}")
2956
- print(f" or detach (Ctrl-b d) then `omega` again.")
2959
+ import subprocess as _sp
2960
+ # Window of Omega session, or standalone session — try
2961
+ # select-window first (for Omega:<name>), then switch-client.
2962
+ if ":" in session_name:
2963
+ sess, win = session_name.split(":", 1)
2964
+ rc = _sp.run(["tmux", "select-window", "-t", session_name],
2965
+ capture_output=True).returncode
2966
+ if rc != 0:
2967
+ _sp.run(["tmux", "switch-client", "-t", sess],
2968
+ capture_output=True)
2969
+ _sp.run(["tmux", "select-window", "-t", session_name],
2970
+ capture_output=True)
2971
+ else:
2972
+ _sp.run(["tmux", "switch-client", "-t", session_name],
2973
+ capture_output=True)
2957
2974
  return 0
2958
2975
  # Outside tmux — replace this process with `tmux attach`.
2959
2976
  print(f" attaching to {label} ({session_name})…")
@@ -3577,9 +3594,21 @@ def cmd_hermes(args: argparse.Namespace) -> int:
3577
3594
  if sub == "chat-loop":
3578
3595
  # REPL that runs inside the Hermes-chat tmux session.
3579
3596
  # Uses Anthropic API directly (separate from AISB's Max OAuth).
3597
+ #
3598
+ # v0.19.53 — DO NOT `import os, sys` here. Both are already
3599
+ # imported at module scope (lines 11–12). Re-importing them
3600
+ # inside this branch made Python treat `sys` and `os` as
3601
+ # local-to-`cmd_hermes` for the WHOLE function — so the
3602
+ # `ask` / `env` / `brief observe` / help-fallback branches
3603
+ # then crashed with `UnboundLocalError: cannot access local
3604
+ # variable 'sys'` when their try/except caught a HermesError
3605
+ # and tried `print(..., file=sys.stderr)`. End result for a
3606
+ # fresh Mac install: `omega hermes ask hi` printed a raw
3607
+ # traceback instead of `error: \`hermes\` not on PATH — run
3608
+ # \`omega hermes install\``. The user reported "hermes doesn't
3609
+ # work" because of this misleading crash.
3580
3610
  from omega_engine.aisb_chat import chat_once
3581
3611
  from omega_engine.vault import vault_read
3582
- import os, sys
3583
3612
  home = _omega_home()
3584
3613
  api_key = (vault_read(home, "ANTHROPIC_API_KEY_HERMES") or "").strip()
3585
3614
  if not api_key:
@@ -20,7 +20,13 @@ DEFAULT_PROVIDER = "claude_code"
20
20
 
21
21
 
22
22
  def _omega_home() -> Path:
23
- return Path(os.environ.get("OMEGA_HOME", str(Path.home() / "Omega")))
23
+ # v0.19.53 — avoid Path.home() on Mac uv-Python (slow: 200-500ms
24
+ # via pwd.getpwuid() + DirectoryService lookup). Use $HOME env
25
+ # directly via os.path.expanduser — milliseconds.
26
+ omega = os.environ.get("OMEGA_HOME")
27
+ if omega:
28
+ return Path(omega)
29
+ return Path(os.path.expanduser("~/Omega"))
24
30
 
25
31
 
26
32
  def provider_state_path() -> Path:
@@ -631,6 +631,13 @@ bind-key S choose-tree -Zs
631
631
  bind-key -n M-/ display-popup -E -w 100% -h 100% "__OMEGA_BIN__ tmux switcher"
632
632
  bind-key -n M-z display-popup -E -w 100% -h 100% "__OMEGA_BIN__ menu-tui"
633
633
 
634
+ # Fallback for terminals that don't send Esc+ on Option+key (e.g. macOS
635
+ # Terminal.app without "Option as Meta", Termius default). These bind
636
+ # the LITERAL Unicode character that Option+Z / Option+/ produces.
637
+ # Safe — these characters are extremely rarely typed intentionally.
638
+ bind-key -n "Ω" display-popup -E -w 100% -h 100% "__OMEGA_BIN__ menu-tui"
639
+ bind-key -n "÷" display-popup -E -w 100% -h 100% "__OMEGA_BIN__ tmux switcher"
640
+
634
641
  # ════════════════════════════════════════════════════════════════════
635
642
  # Pixel/CRT amber theme (tmux-claude inspired)
636
643
  # xterm colour137 ≈ #af8700 — amber/gold on whatever your terminal bg is.
@@ -768,6 +775,13 @@ bind-key r source-file ~/.tmux.conf \\; display-message "tmux.conf reloaded"
768
775
  bind-key -n M-/ display-popup -E -w 100% -h 100% "__OMEGA_BIN__ tmux switcher"
769
776
  bind-key -n M-z display-popup -E -w 100% -h 100% "__OMEGA_BIN__ menu-tui"
770
777
 
778
+ # Fallback for terminals that don't send Esc+ on Option+key (e.g. macOS
779
+ # Terminal.app without "Option as Meta", Termius default). These bind
780
+ # the LITERAL Unicode character that Option+Z / Option+/ produces.
781
+ # Safe — these characters are extremely rarely typed intentionally.
782
+ bind-key -n "Ω" display-popup -E -w 100% -h 100% "__OMEGA_BIN__ menu-tui"
783
+ bind-key -n "÷" display-popup -E -w 100% -h 100% "__OMEGA_BIN__ tmux switcher"
784
+
771
785
  # ════════════════════════════════════════════════════════════════════
772
786
  # Pixel/CRT amber theme (tmux-claude inspired)
773
787
  # ════════════════════════════════════════════════════════════════════
@@ -48,16 +48,26 @@ import shlex
48
48
  from pathlib import Path
49
49
  from typing import Any
50
50
 
51
- try:
52
- from textual.app import App, ComposeResult
53
- from textual.widgets import (
54
- Header, Footer, Input, RichLog, ListView, ListItem, Label,
55
- )
56
- from textual.containers import Horizontal, Vertical
57
- from textual.binding import Binding
58
- TEXTUAL_AVAILABLE = True
59
- except ImportError:
60
- TEXTUAL_AVAILABLE = False
51
+ # v0.19.53 — defer the textual import to runtime. Importing
52
+ # ``textual.app`` triggers ~80-150ms of import cost on macOS uv-Python (60+
53
+ # transitive modules: rich, markupsafe, jinja-ish, etc.). The default
54
+ # ``omega`` UX is the fzf arrow-key menu (``_arrow_menu``), which does NOT
55
+ # need textual at all. We only need it when the user explicitly opts in
56
+ # with ``--textual``. So we probe lazily and build the class on demand.
57
+ TEXTUAL_AVAILABLE: bool | None = None # tri-state: None = not probed yet
58
+
59
+
60
+ def _probe_textual() -> bool:
61
+ """Return True iff the ``textual`` package is importable. Caches the
62
+ answer so the probe only runs once per process."""
63
+ global TEXTUAL_AVAILABLE
64
+ if TEXTUAL_AVAILABLE is None:
65
+ try:
66
+ import importlib.util as _util
67
+ TEXTUAL_AVAILABLE = _util.find_spec("textual") is not None
68
+ except Exception: # noqa: BLE001
69
+ TEXTUAL_AVAILABLE = False
70
+ return TEXTUAL_AVAILABLE
61
71
 
62
72
 
63
73
  # ---------------------------------------------------------------------------
@@ -256,7 +266,29 @@ def dispatch_slash(line: str) -> CommandResult:
256
266
  # ---------------------------------------------------------------------------
257
267
 
258
268
 
259
- if TEXTUAL_AVAILABLE:
269
+ def _build_omega_tui_class():
270
+ """Build the ``OmegaTUI`` class on first request.
271
+
272
+ v0.19.53 — previously the class lived at module level inside
273
+ ``if TEXTUAL_AVAILABLE: class OmegaTUI(App): ...``, which forced
274
+ ``textual.app`` (+ 60 transitive modules including ``rich``) to load
275
+ every time ``omega_engine.tui`` was imported — even for the default
276
+ fzf arrow-key menu that never touches textual.
277
+
278
+ Building the class on demand keeps the default ``omega`` startup path
279
+ free of the textual import cost. The class is cached on a function
280
+ attribute so subsequent calls return the same class.
281
+ """
282
+ cached = getattr(_build_omega_tui_class, "_cls", None)
283
+ if cached is not None:
284
+ return cached
285
+
286
+ from textual.app import App, ComposeResult
287
+ from textual.widgets import (
288
+ Header, Footer, Input, RichLog, ListView, ListItem, Label,
289
+ )
290
+ from textual.containers import Horizontal, Vertical
291
+ from textual.binding import Binding
260
292
 
261
293
  class OmegaTUI(App):
262
294
  """OmegaOS Textual TUI — the v0.19.27 control surface."""
@@ -409,6 +441,9 @@ if TEXTUAL_AVAILABLE:
409
441
  if res.session_jump:
410
442
  _switch_client(res.session_jump)
411
443
 
444
+ _build_omega_tui_class._cls = OmegaTUI # type: ignore[attr-defined]
445
+ return OmegaTUI
446
+
412
447
 
413
448
  def _arrow_menu() -> int:
414
449
  """v0.19.30 — fzf-driven arrow-key menu (the default `omega` UX).
@@ -962,8 +997,9 @@ def run_tui(prefer_textual: bool = False,
962
997
  - `--repl` → `_plain_repl()` (input() loop, slash commands)
963
998
  - `--textual` → Textual TUI (opt-in, may break under tmux)
964
999
  """
965
- if prefer_textual and TEXTUAL_AVAILABLE:
1000
+ if prefer_textual and _probe_textual():
966
1001
  try:
1002
+ OmegaTUI = _build_omega_tui_class()
967
1003
  app = OmegaTUI()
968
1004
  app.run()
969
1005
  return 0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "omega-engine"
3
- version = "0.19.52"
3
+ version = "0.19.54"
4
4
  description = "The Omega OS orchestration engine — event-sourced, verified-completion agent graphs."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -247,6 +247,19 @@ class TestTmuxBindsFixedAndPathsAbsolute(unittest.TestCase):
247
247
  self.assertIn('"Omega:hermes"', src,
248
248
  "spawn_hermes_chat must return 'Omega:hermes'")
249
249
 
250
+ def test_unicode_fallback_binds_for_macos_terminal(self):
251
+ """v0.19.53 — Option+Z / Option+/ on macOS Terminal.app (without
252
+ Option=Meta config) sends Unicode Ω / ÷. Bind those characters
253
+ too as a fallback so the menu fires anyway."""
254
+ from omega_engine.tmux import _PRO_CONFIG, _DEFAULT_CONFIG
255
+ for name, cfg in (("_PRO_CONFIG", _PRO_CONFIG),
256
+ ("_DEFAULT_CONFIG", _DEFAULT_CONFIG)):
257
+ self.assertIn('"Ω"', cfg,
258
+ f"{name} must bind the literal Ω character (Option+Z on macOS) "
259
+ f"as a fallback for terminals without Option=Meta config")
260
+ self.assertIn('"÷"', cfg,
261
+ f"{name} must bind the literal ÷ character (Option+/ on macOS)")
262
+
250
263
 
251
264
  if __name__ == "__main__":
252
265
  unittest.main(verbosity=2)
@@ -0,0 +1,351 @@
1
+ """Regression tests for v0.19.53 — ``omega hermes`` and ``omega paperclip``
2
+ must produce user-friendly output (or open the chat session), NEVER crash
3
+ with a raw Python traceback.
4
+
5
+ User-reported bug on a fresh Mac install (v0.19.51): both commands
6
+ "don't work". Diagnosis traced two distinct issues:
7
+
8
+ Bug 1 — UnboundLocalError in ``cmd_hermes``
9
+ --------------------------------------------
10
+ ``omega hermes ask "anything"`` crashed with::
11
+
12
+ UnboundLocalError: cannot access local variable 'sys' where it
13
+ is not associated with a value
14
+
15
+ Root cause: the ``chat-loop`` branch had ``import os, sys`` inside
16
+ the function. Python's static-binding rule then treated ``sys``
17
+ (and ``os``) as local to the WHOLE function — so the ``ask`` /
18
+ ``env`` / ``brief observe`` / help-fallback branches blew up when
19
+ their ``except HermesError`` handler tried ``print(..., file=
20
+ sys.stderr)``.
21
+
22
+ Visible consequence on a fresh install (where Hermes is not yet
23
+ installed, so ``H.ask`` raises ``HermesError``): the user saw a
24
+ raw 15-line Python traceback instead of the intended friendly
25
+ ``error: \\`hermes\\` not on PATH — run \\`omega hermes install\\``.
26
+
27
+ Fix (cli.py only): remove the redundant ``import os, sys`` from
28
+ the ``chat-loop`` branch — both are already imported at module
29
+ scope.
30
+
31
+ Bug 2 — paperclip works but the test surface had no regression
32
+ ---------------------------------------------------------------
33
+ ``omega paperclip`` (no subcommand) and ``omega paperclip status``
34
+ already produce friendly output on an un-installed Paperclip
35
+ (defaulting to ``status`` via ``sub = ... or "status"``). We lock
36
+ that behaviour in so a future refactor cannot regress it.
37
+
38
+ Each test exercises the user-visible behaviour, not implementation
39
+ details — a later refactor of the friendly-error wording is fine as
40
+ long as the command still exits cleanly with help, not a traceback.
41
+ """
42
+ from __future__ import annotations
43
+
44
+ import io
45
+ import os
46
+ import sys
47
+ import tempfile
48
+ import unittest
49
+ from contextlib import redirect_stderr, redirect_stdout
50
+ from pathlib import Path
51
+ from unittest import mock
52
+
53
+
54
+ HERE = Path(__file__).resolve().parent
55
+ ENGINE_ROOT = HERE.parent
56
+
57
+ # Make sure ``omega_engine`` is importable even when pytest is run from
58
+ # the project root.
59
+ sys.path.insert(0, str(ENGINE_ROOT))
60
+
61
+
62
+ def _run_omega(argv: list[str]) -> tuple[int, str, str]:
63
+ """Invoke ``omega_engine.cli.main`` in-process with the given argv.
64
+
65
+ Returns ``(rc, stdout, stderr)``. Swallows ``SystemExit`` so callers
66
+ can assert on the exit code without an exception bubbling up.
67
+ """
68
+ from omega_engine.cli import main
69
+
70
+ out, err = io.StringIO(), io.StringIO()
71
+ rc = 0
72
+ saved_argv = sys.argv[:]
73
+ sys.argv = ["omega"] + argv
74
+ try:
75
+ with redirect_stdout(out), redirect_stderr(err):
76
+ try:
77
+ rc = main()
78
+ except SystemExit as exc: # argparse exits via SystemExit
79
+ rc = int(exc.code or 0)
80
+ finally:
81
+ sys.argv = saved_argv
82
+ return rc, out.getvalue(), err.getvalue()
83
+
84
+
85
+ class TestHermesCLI(unittest.TestCase):
86
+ """`omega hermes ...` must never print a raw traceback."""
87
+
88
+ def setUp(self) -> None:
89
+ self._tmp = tempfile.mkdtemp(prefix="omega-test-")
90
+ self._home = Path(self._tmp) / "Omega"
91
+ (self._home / "Agentik_Extra" / "var" / "logs").mkdir(
92
+ parents=True, exist_ok=True
93
+ )
94
+ self._env_patch = mock.patch.dict(
95
+ os.environ, {"OMEGA_HOME": str(self._home)}, clear=False,
96
+ )
97
+ self._env_patch.start()
98
+
99
+ def tearDown(self) -> None:
100
+ self._env_patch.stop()
101
+ import shutil as _sh
102
+ _sh.rmtree(self._tmp, ignore_errors=True)
103
+
104
+ # ------------------------------------------------------------------
105
+ # Bug 1 — UnboundLocalError regression (the headline fix)
106
+ # ------------------------------------------------------------------
107
+ def test_omega_hermes_ask_with_no_hermes_installed_gives_friendly_error(
108
+ self,
109
+ ) -> None:
110
+ """When hermes is not on PATH (fresh install), ``omega hermes ask``
111
+ MUST print a friendly one-liner and exit non-zero — NOT crash
112
+ with ``UnboundLocalError: ... 'sys'``.
113
+ """
114
+ from omega_engine import hermes as H
115
+
116
+ # Simulate "hermes binary not installed" — ``H.ask`` raises
117
+ # HermesError. The cli handler must catch it and print a hint.
118
+ with mock.patch.object(
119
+ H, "ask",
120
+ side_effect=H.HermesError(
121
+ "`hermes` not on PATH — run `omega hermes install`"
122
+ ),
123
+ ):
124
+ rc, out, err = _run_omega(["hermes", "ask", "hi"])
125
+
126
+ # The bug was UnboundLocalError — surface it loudly if it ever
127
+ # comes back.
128
+ full = out + err
129
+ self.assertNotIn("UnboundLocalError", full,
130
+ msg=f"UnboundLocalError regression:\n{full}")
131
+ self.assertNotIn("Traceback", full,
132
+ msg=f"raw traceback leaked to user:\n{full}")
133
+ # Friendly hint must be on stderr (where the handler writes it).
134
+ self.assertIn("hermes", err.lower())
135
+ # And we exit with rc=2 (the existing contract for an ask failure).
136
+ self.assertEqual(rc, 2)
137
+
138
+ def test_omega_hermes_env_friendly_error_when_creds_unavailable(self) -> None:
139
+ """`omega hermes env` MUST handle ``HermesError`` from ``build_env``
140
+ without crashing on ``sys`` being unbound.
141
+ """
142
+ from omega_engine import hermes as H
143
+
144
+ with mock.patch.object(
145
+ H, "build_env",
146
+ side_effect=H.HermesError("no Claude OAuth token found"),
147
+ ):
148
+ rc, out, err = _run_omega(["hermes", "env"])
149
+
150
+ full = out + err
151
+ self.assertNotIn("UnboundLocalError", full)
152
+ self.assertNotIn("Traceback", full)
153
+ self.assertEqual(rc, 2)
154
+ self.assertIn("no Claude OAuth token", err)
155
+
156
+ # ------------------------------------------------------------------
157
+ # `omega hermes status` — should always work, even without hermes
158
+ # ------------------------------------------------------------------
159
+ def test_omega_hermes_status_renders_without_crashing(self) -> None:
160
+ """`omega hermes status` reads the in-process probe and prints a
161
+ snapshot. It must succeed even if hermes is not installed.
162
+ """
163
+ rc, out, err = _run_omega(["hermes", "status"])
164
+
165
+ self.assertNotIn("UnboundLocalError", out + err)
166
+ self.assertNotIn("Traceback", out + err)
167
+ # Exit code 0 (all green) or 1 (something missing) — both are
168
+ # legitimate; what matters is no crash.
169
+ self.assertIn(rc, (0, 1))
170
+ # Output mentions the canonical fields.
171
+ self.assertIn("hermes", out.lower())
172
+
173
+ def test_omega_hermes_status_json_emits_parsable_json(self) -> None:
174
+ """`omega hermes status --json` must emit valid JSON, not a
175
+ traceback — used by external tooling.
176
+ """
177
+ import json as _json
178
+
179
+ rc, out, err = _run_omega(["hermes", "status", "--json"])
180
+ self.assertNotIn("Traceback", out + err)
181
+ self.assertIn(rc, (0, 1))
182
+ # The JSON object lives on stdout. Parse the first complete
183
+ # JSON document we find (the handler emits one indented object).
184
+ try:
185
+ parsed = _json.loads(out)
186
+ except _json.JSONDecodeError as exc:
187
+ self.fail(f"--json did not produce parseable JSON: {exc}\n{out!r}")
188
+ self.assertIn("installed", parsed)
189
+ self.assertIn("hermes_home", parsed)
190
+
191
+ # ------------------------------------------------------------------
192
+ # `omega hermes install --dry-run` — must not actually run install.sh
193
+ # ------------------------------------------------------------------
194
+ def test_omega_hermes_install_dry_run_prints_command(self) -> None:
195
+ """`omega hermes install --dry-run` prints the curl command it
196
+ WOULD run, exits 0, and does NOT crash.
197
+ """
198
+ rc, out, err = _run_omega(["hermes", "install", "--dry-run"])
199
+ self.assertNotIn("Traceback", out + err)
200
+ self.assertEqual(rc, 0)
201
+ # Either "dry-run" appears or the install URL — both prove the
202
+ # branch executed.
203
+ self.assertTrue(
204
+ "dry-run" in out.lower() or "github" in out.lower(),
205
+ msg=f"unexpected output:\n{out}",
206
+ )
207
+
208
+
209
+ class TestPaperclipCLI(unittest.TestCase):
210
+ """`omega paperclip` must default to a useful status snapshot, and
211
+ its subcommands must never blow up with a Python traceback."""
212
+
213
+ def setUp(self) -> None:
214
+ self._tmp = tempfile.mkdtemp(prefix="omega-test-")
215
+ self._home = Path(self._tmp) / "Omega"
216
+ self._pc_home = Path(self._tmp) / ".paperclip"
217
+ (self._home / "Agentik_Extra" / "var" / "logs").mkdir(
218
+ parents=True, exist_ok=True
219
+ )
220
+ self._env_patch = mock.patch.dict(
221
+ os.environ,
222
+ {
223
+ "OMEGA_HOME": str(self._home),
224
+ "PAPERCLIP_HOME": str(self._pc_home),
225
+ },
226
+ clear=False,
227
+ )
228
+ self._env_patch.start()
229
+
230
+ def tearDown(self) -> None:
231
+ self._env_patch.stop()
232
+ import shutil as _sh
233
+ _sh.rmtree(self._tmp, ignore_errors=True)
234
+
235
+ def test_omega_paperclip_no_args_runs_status_without_crashing(self) -> None:
236
+ """Bare ``omega paperclip`` (no subcommand) MUST default to
237
+ ``status`` and render a snapshot — not crash, not show argparse
238
+ help (which exits non-zero from the user's POV).
239
+ """
240
+ rc, out, err = _run_omega(["paperclip"])
241
+ self.assertNotIn("Traceback", out + err)
242
+ self.assertEqual(rc, 0)
243
+ # Status output mentions the canonical fields.
244
+ self.assertIn("paperclip", out.lower())
245
+ self.assertIn("agents registered", out.lower())
246
+
247
+ def test_omega_paperclip_status_renders_canonical_fields(self) -> None:
248
+ """`omega paperclip status` always prints the 7 canonical fields:
249
+ installed / paperclip_home / omegaos company / agents registered /
250
+ workspaces / heartbeats pending / bridge_version."""
251
+ rc, out, err = _run_omega(["paperclip", "status"])
252
+ self.assertNotIn("Traceback", out + err)
253
+ self.assertEqual(rc, 0)
254
+ low = out.lower()
255
+ for needle in (
256
+ "paperclip installed",
257
+ "paperclip_home",
258
+ "omegaos company",
259
+ "agents registered",
260
+ "workspaces",
261
+ "heartbeats pending",
262
+ "bridge_version",
263
+ ):
264
+ self.assertIn(needle, low,
265
+ msg=f"missing field {needle!r} in:\n{out}")
266
+
267
+ def test_omega_paperclip_register_dry_run_writes_14_agents(self) -> None:
268
+ """`omega paperclip register --dry-run` must report 14 agents
269
+ written (Hermès + 13 AISB suite — the contract from v0.19.33)."""
270
+ rc, out, err = _run_omega(["paperclip", "register", "--dry-run"])
271
+ self.assertNotIn("Traceback", out + err)
272
+ self.assertEqual(rc, 0)
273
+ # Output is `key: value` lines from PB.register's summary dict.
274
+ self.assertIn("agents_written: 14", out)
275
+ self.assertIn("dry_run: True", out)
276
+
277
+ def test_omega_paperclip_url_default_loopback(self) -> None:
278
+ """`omega paperclip url` prints a dashboard URL, no traceback."""
279
+ rc, out, err = _run_omega(["paperclip", "url"])
280
+ self.assertNotIn("Traceback", out + err)
281
+ self.assertEqual(rc, 0)
282
+ self.assertIn("paperclip dashboard", out.lower())
283
+
284
+ def test_omega_paperclip_remote_setup_prints_help(self) -> None:
285
+ """`omega paperclip remote-setup` prints the SSH/Tailscale/Tunnel
286
+ instructions without crashing."""
287
+ rc, out, err = _run_omega(["paperclip", "remote-setup"])
288
+ self.assertNotIn("Traceback", out + err)
289
+ self.assertEqual(rc, 0)
290
+ self.assertGreater(len(out.strip()), 0,
291
+ msg="remote-setup produced no output")
292
+
293
+
294
+ class TestCmdHermesNoLocalImports(unittest.TestCase):
295
+ """White-box guard: ensure ``cmd_hermes`` never re-imports ``sys`` or
296
+ ``os`` in a way that would shadow the module-level imports.
297
+
298
+ This is the structural invariant that catches Bug 1 at the source.
299
+ If a future commit adds ``import sys`` (or ``import os, sys``, or
300
+ ``from sys import ...``) inside ``cmd_hermes``, this test fails
301
+ immediately, with a clear pointer to the v0.19.53 root cause.
302
+ """
303
+
304
+ def test_cmd_hermes_does_not_reimport_sys_or_os_locally(self) -> None:
305
+ import ast
306
+ import inspect
307
+
308
+ from omega_engine import cli as _cli
309
+
310
+ src = inspect.getsource(_cli.cmd_hermes)
311
+ # Trim leading indentation so ast.parse can handle the snippet.
312
+ import textwrap
313
+ tree = ast.parse(textwrap.dedent(src))
314
+
315
+ offenders: list[str] = []
316
+
317
+ class _Visitor(ast.NodeVisitor):
318
+ def visit_Import(self, node: ast.Import) -> None: # noqa: N802
319
+ for alias in node.names:
320
+ if alias.name in {"sys", "os"} and alias.asname is None:
321
+ offenders.append(
322
+ f"line {node.lineno}: `import {alias.name}` "
323
+ f"shadows module-level import"
324
+ )
325
+
326
+ def visit_ImportFrom( # noqa: N802
327
+ self, node: ast.ImportFrom,
328
+ ) -> None:
329
+ if node.module in {"sys", "os"}:
330
+ for alias in node.names:
331
+ if alias.asname is None and alias.name != "*":
332
+ offenders.append(
333
+ f"line {node.lineno}: "
334
+ f"`from {node.module} import {alias.name}`"
335
+ )
336
+
337
+ _Visitor().visit(tree)
338
+ self.assertEqual(
339
+ offenders, [],
340
+ msg=(
341
+ "cmd_hermes re-imports `sys` / `os` locally — this "
342
+ "shadows the module-level imports and breaks branches "
343
+ "that reference them in `except` handlers (the v0.19.53 "
344
+ "UnboundLocalError bug). Offenders:\n "
345
+ + "\n ".join(offenders)
346
+ ),
347
+ )
348
+
349
+
350
+ if __name__ == "__main__": # pragma: no cover
351
+ unittest.main()
@@ -1 +1 @@
1
- 0.19.52
1
+ 0.19.54
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentikos/omega-os",
3
- "version": "0.19.52",
3
+ "version": "0.19.54",
4
4
  "description": "Omega OS — installable agentic operating system with verified-completion orchestration. Event-sourced engine, 8-block rack, autonomous agents, MCP.",
5
5
  "bin": {
6
6
  "omega-os": "bin/omega-os.js"