diff --git a/.gitignore b/.gitignore index de39268d..3e4a76cc 100644 --- a/.gitignore +++ b/.gitignore @@ -22,9 +22,6 @@ audits/.*/ __pycache__/ *.pyc -# OmegaMC dashboard — separate repo (agentik-os/agentik-telegram), MIT (M. Tzanidakis) -mission-control/ - # OmegaOS hygiene — agent/test artifacts (auto-added) .audit/ .code/ diff --git a/agents/aisb-atlas.md b/agents/aisb-atlas.md index 177f05d6..c620fb71 100644 --- a/agents/aisb-atlas.md +++ b/agents/aisb-atlas.md @@ -15,7 +15,7 @@ You are the single entry point. You directly direct two groups: - HUMAN (operator) talks to YOU on Telegram. - ATLAS (you) — boss of the AISB team. You set direction, priorities, and standards, then dispatch. - - **14 MATRIX MANAGERS** (the AISB agents, in Mission Control): oracle, + - **14 MATRIX MANAGERS** (the AISB agents, Telegram personas — not worker processes): oracle, morpheus, seraph, keymaker, niobe, smith, architect, merovingian, neo, zion, link, construct, pythia, council. - **PROJECT ORACLES** — ONE dedicated oracle per project (multi-session: @@ -35,8 +35,8 @@ You are the single entry point. You directly direct two groups: 4. **System evolution** — via SMITH (patterns) + MEROVINGIAN (cross-project knowledge): turn finished-mission lessons into better doctrine/skills/installer (R-INSTALLER / L0 install-parity). -5. **Project lifecycle** — when a project is added it gets a dedicated oracle, a - Telegram topic, and a Mission-Control entry; messages in a project's topic are +5. **Project lifecycle** — when a project is added it gets a dedicated oracle and a + Telegram topic; messages in a project's topic are about THAT project — direct its oracle. ## How you operate diff --git a/agents/aisb/atlas.md b/agents/aisb/atlas.md index 110a652f..a10e9afe 100644 --- a/agents/aisb/atlas.md +++ b/agents/aisb/atlas.md @@ -41,7 +41,7 @@ set direction; the Master executes it. (R-VERIFY, ≥2-of-3). 4. **System evolution** — via SMITH (patterns) + MEROVINGIAN (cross-project knowledge): turn finished-mission lessons into better doctrine/skills/installer. -5. **Oversight** — `~/.omega/state/oracle-*.done.json`, the dashboard, `omega doctor`. +5. **Oversight** — `~/.omega/state/oracle-*.done.json`, Telegram / TUI, `omega doctor`. ## How you operate diff --git a/crates/omega-cli/src/main.rs b/crates/omega-cli/src/main.rs index 0575eb2e..4e27404b 100644 --- a/crates/omega-cli/src/main.rs +++ b/crates/omega-cli/src/main.rs @@ -8425,7 +8425,7 @@ async fn cmd_spawn_worker( dir: Option<&str>, project: Option<&str>, files: Option>, - force: bool, + _force: bool, worktree: bool, agent_override: Option<&str>, ) -> Result<()> { @@ -8443,12 +8443,7 @@ async fn cmd_spawn_worker( // oracle. Resolve the real name by asking rmux to expand #{session_name} for // our pane ($RMUX_PANE). let oracle_session = current_session_name().filter(|s| s.starts_with("oracle-")); - let prompt_owned = if oracle_session.is_some() { - omega_core::lab::ensure_oracle_worker_rubric(prompt, task) - } else { - prompt.to_string() - }; - let prompt = prompt_owned.as_str(); + omega_core::lab::require_worker_rubric(prompt).map_err(anyhow::Error::msg)?; let project_name = match project { Some(p) => Some(p.to_string()), @@ -8485,27 +8480,7 @@ async fn cmd_spawn_worker( None => format!("worker-{}", task), }); - // Worker-prompt completeness gate (cheap, no LLM): the brief MUST carry both a - // Done-criteria signal AND a Verify-command signal. Checked BEFORE the scope - // claim so a rejected dispatch never leaves a file lock behind. - let prompt_lc = prompt.to_lowercase(); - let has_done = prompt_lc.contains("done criteria") - || prompt_lc.contains("done:") - || prompt_lc.contains("done-criteria"); - let has_verify = prompt_lc.contains("verify"); - if !(has_done && has_verify) { - let missing = match (has_done, has_verify) { - (false, false) => "Done Criteria + Verify Command", - (false, true) => "Done Criteria", - (true, false) => "Verify Command", - (true, true) => unreachable!(), - }; - anyhow::bail!( - "worker prompt missing {missing}. Add explicit \"Done Criteria:\" and a \"Verify Command:\" \ - when you write the prompt (rule R-RUBRIC). --force does not skip this{}.", - if force { " (ignored)" } else { "" } - ); - } + // R-RUBRIC already refused missing/unsafe briefs above. --force does not skip it. let agent = match agent_override { Some(name) => { diff --git a/crates/omega-core/src/agents.rs b/crates/omega-core/src/agents.rs index 1171dd9c..9fccc97a 100644 --- a/crates/omega-core/src/agents.rs +++ b/crates/omega-core/src/agents.rs @@ -527,7 +527,7 @@ impl Agent { providers.claude.dangerously_skip_permissions, )?; let mut args = format!( - "{}{}exec CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1 claude{}", + "{}{}CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1 exec claude{}", env_prefix, trust_prefix, permission_args ); if let Some(ref sys_file) = opts.system_prompt_file { @@ -670,7 +670,7 @@ impl Agent { "--sandbox workspace-write --ask-for-approval on-request" }; let mut args = format!( - "{}{}exec COLORFGBG='15;0' codex --strict-config {}", + "{}{}COLORFGBG='15;0' exec codex --strict-config {}", env_prefix, trust_prefix, approval ); if providers.codex.bypass_hook_trust { @@ -819,10 +819,9 @@ impl Agent { } } Agent::Hermes => { - // Hermes has required an explicit `chat` subcommand for - // one-shot prompts since before v0.20. A bare positional prompt - // is parsed as an invalid subcommand. Keep no-prompt sessions - // interactive and use the documented query lane for dispatch. + // Hermes requires the `chat` subcommand. A bare positional prompt + // is an unrecognized argument and kills the pane. Keep sessions + // interactive; never `-q` (one-shot exit). let hermes_provider = if !providers.hermes.provider.trim().is_empty() { Some(providers.hermes.provider.trim()) } else if !providers.hermes.api_key.is_empty() @@ -850,27 +849,21 @@ impl Agent { // blocking a detached pane. Never `-q`: that is a one-shot // query that exits and used to drop the pane to bash. let yolo_arg = if providers.hermes.yolo { " --yolo" } else { "" }; + // Assignments MUST precede `exec`. `exec HERMES_YOLO_MODE=1 hermes` + // is `exec` of a command named HERMES_YOLO_MODE=1 → pane dies. let yolo_env = if providers.hermes.yolo { "HERMES_YOLO_MODE=1 " } else { "" }; - match initial_prompt { - Some(p) => pane_bash(&format!( - "{}exec {}hermes chat{}{}{}{} {}", - env_prefix, - yolo_env, - provider_arg, - hermes_args, - yolo_arg, - resume_arg, - shell_quote(p) - )), - None => pane_bash(&format!( - "{}exec {}hermes chat{}{}{}{}", - env_prefix, yolo_env, provider_arg, hermes_args, yolo_arg, resume_arg - )), - } + // Hermes chat has no positional prompt (unrecognized arguments → + // exit). `-q` is a one-shot that also exits. Home/TUI panes stay + // on interactive `chat`; callers inject the first message after + // the TUI is up. + pane_bash(&format!( + "{}{}exec hermes chat{}{}{}{}", + env_prefix, yolo_env, provider_arg, hermes_args, yolo_arg, resume_arg + )) } Agent::Glm => { // GLM (Z.AI/Zhipu) = Claude Code redirected to Z.AI's Anthropic- @@ -899,7 +892,7 @@ impl Agent { }; match initial_prompt { Some(p) => pane_bash(&format!( - "{} {}exec CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1 claude{}{}{} {}", + "{} {}CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1 exec claude{}{}{} {}", env_prefix, trust_prefix, perms, @@ -908,7 +901,7 @@ impl Agent { shell_quote(p) )), None => pane_bash(&format!( - "{} {}exec CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1 claude{}{}{}", + "{} {}CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1 exec claude{}{}{}", env_prefix, trust_prefix, perms, model_arg, resume_arg )), } @@ -1216,7 +1209,9 @@ mod tests { && cmd.contains("never") && !cmd.contains("--approve-for-me") && !cmd.contains("; exec bash") - && cmd.contains("exec COLORFGBG=") + && cmd.contains("COLORFGBG=") + && cmd.contains(" exec codex ") + && !cmd.contains("exec COLORFGBG=") && cmd.contains("--add-dir") && cmd.contains("--dangerously-bypass-hook-trust") && cmd.contains("--no-alt-screen"), @@ -1253,6 +1248,46 @@ mod tests { ); } + #[test] + fn exec_does_not_swallow_environment_assignments() { + // Live 2026-08-25: `exec VAR=value cmd` → bash: exec: VAR=value: not found + // (exit 127). Every Home pane (Claude / Codex / Hermes) died on launch. + for agent in [Agent::Claude, Agent::Codex, Agent::Hermes, Agent::Glm] { + let cmd = launch(agent, None, LaunchOptions::default()); + assert!( + !cmd.contains("exec CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN="), + "bash exec cannot take env assignments: {cmd}" + ); + assert!( + !cmd.contains("exec COLORFGBG="), + "bash exec cannot take env assignments: {cmd}" + ); + assert!( + !cmd.contains("exec HERMES_YOLO_MODE="), + "bash exec cannot take env assignments: {cmd}" + ); + } + let claude = launch(Agent::Claude, None, LaunchOptions::default()); + assert!( + claude.contains("CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1 exec claude"), + "{claude}" + ); + let hermes = launch(Agent::Hermes, None, LaunchOptions::default()); + assert!( + hermes.contains("HERMES_YOLO_MODE=1 exec hermes chat"), + "{hermes}" + ); + let hermes_prompt = launch( + Agent::Hermes, + Some("inspect the repository"), + LaunchOptions::default(), + ); + assert!( + !hermes_prompt.contains("inspect the repository"), + "hermes chat has no positional prompt (unrecognized arguments): {hermes_prompt}" + ); + } + #[test] fn agent_pane_is_the_agent_not_a_bash_fallback() { for agent in [ diff --git a/crates/omega-core/src/dispatch.rs b/crates/omega-core/src/dispatch.rs index 9f4f20a6..1148a283 100644 --- a/crates/omega-core/src/dispatch.rs +++ b/crates/omega-core/src/dispatch.rs @@ -648,11 +648,13 @@ fn resolve_dispatch_agent( crate::external_orchestrator::resolve_mission_writer(agent_override, configured) } -fn seed_lab_plan(state_dir: &Path, oracle_name: &str) -> Result<()> { +fn seed_lab_plan(state_dir: &Path, oracle_name: &str, mission: &str) -> Result<()> { + let steps = crate::lab::lab_plan_for_mission(mission); let mut todo = crate::oracle_todo::OracleTodo::load(state_dir, oracle_name)?; - todo.set_plan(crate::lab::LAB_LOOP_STEPS.iter().copied()); + todo.set_plan(steps.iter().copied()); + let first = steps.first().copied().unwrap_or("Understand"); let _ = todo.upsert( - "Understand", + first, crate::oracle_todo::TodoStatus::Doing, Some("seeded by omega dispatch — AGK Agentic Engineering Lab loop"), ); @@ -1442,7 +1444,7 @@ impl Dispatcher { let session_id = gen_session_uuid(); oracle_state.session_id = Some(session_id.clone()); oracle_state.write(&self.config.state_dir)?; - seed_lab_plan(&self.config.state_dir, &oracle_name).with_context(|| { + seed_lab_plan(&self.config.state_dir, &oracle_name, mission).with_context(|| { format!("seeding Lab plan for {oracle_name} — ANALYSE with 0/0 is not a dispatch") })?; @@ -1540,7 +1542,7 @@ impl Dispatcher { prompt.push_str("\n\n"); prompt.push_str(&compiled.markdown); } - prompt.push_str(&crate::lab::oracle_lab_block()); + prompt.push_str(&crate::lab::oracle_lab_block_for_mission(mission)); // Claude-only smart spawn (2026-w20 features): /goal + --effort + // budget caps. Gemini/GLM/Pi/Hermes fall back to the bare launcher @@ -3434,46 +3436,24 @@ mod ledger_followup_tests { } #[test] - fn seed_lab_plan_writes_eleven_steps_or_fails() { + fn seed_lab_plan_scales_to_mission_or_fails() { let tmp = tempfile::TempDir::new().unwrap(); - let ledger = MissionLedger::open(mission_ledger_path(tmp.path())).unwrap(); - let mission = Mission::new("OmegaOS", "tiny mission", PathBuf::from("/tmp/OmegaOS")); - ledger - .create_mission( - &mission, - &format!("test:{}:created", mission.id.as_str()), - "test", - ) - .unwrap(); - let mut classified = AppendEvent::new( - mission.id.clone(), - 1, - format!("test:{}:classified", mission.id.as_str()), - "test", - "mission_classified", - ); - classified.next_mission_state = Some(MissionState::Classified); - let classified = ledger.append(classified).unwrap(); - let mut state = OracleState::from_ledger("oracle-OmegaOS", &mission, &classified).unwrap(); - assert!( - state.session_id.is_none(), - "from_ledger must not invent a conversation id" - ); - let session_id = gen_session_uuid(); - state.session_id = Some(session_id.clone()); - state.write(tmp.path()).unwrap(); - let loaded = OracleState::read(tmp.path(), "oracle-OmegaOS") - .unwrap() - .unwrap(); + seed_lab_plan(tmp.path(), "oracle-OmegaOS", "tiny typo in the README").unwrap(); + let todo = crate::oracle_todo::OracleTodo::load(tmp.path(), "oracle-OmegaOS").unwrap(); assert_eq!( - loaded.session_id.as_deref(), - Some(session_id.as_str()), - "first persist must carry session_id so ANALYSE is not session_id=null" + todo.tasks.len(), + 3, + "a tiny ask must seed Understand|Build|Verify, not Deploy/Observe" ); - seed_lab_plan(tmp.path(), "oracle-OmegaOS").unwrap(); - let todo = crate::oracle_todo::OracleTodo::load(tmp.path(), "oracle-OmegaOS").unwrap(); - assert_eq!(todo.tasks.len(), 11); assert_eq!(todo.tasks[0].title, "Understand"); assert_eq!(todo.tasks[0].status, crate::oracle_todo::TodoStatus::Doing); + seed_lab_plan( + tmp.path(), + "oracle-OmegaOS", + "complete overhaul of the entire system from scratch", + ) + .unwrap(); + let epic = crate::oracle_todo::OracleTodo::load(tmp.path(), "oracle-OmegaOS").unwrap(); + assert_eq!(epic.tasks.len(), 11); } } diff --git a/crates/omega-core/src/lab.rs b/crates/omega-core/src/lab.rs index 3c5aabba..76510176 100644 --- a/crates/omega-core/src/lab.rs +++ b/crates/omega-core/src/lab.rs @@ -21,30 +21,56 @@ pub const LAB_LOOP_STEPS: &[&str] = &[ "Improve", ]; +/// Small missions do not cargo-cult Deploy/Observe/Improve. +pub const LAB_LOOP_CORE: &[&str] = &["Understand", "Build", "Verify"]; + +/// Medium missions: design + test without a fake deploy phase. +pub const LAB_LOOP_STANDARD: &[&str] = &["Understand", "Design", "Build", "Test", "Verify"]; + /// Pipe-separated plan string for `omega progress --plan`. pub fn lab_plan_spec() -> String { LAB_LOOP_STEPS.join("|") } +/// Scale the Lab loop to the routed complexity of THIS mission. +pub fn lab_plan_for_mission(mission: &str) -> &'static [&'static str] { + use crate::routing::{classify_mission, Complexity}; + match classify_mission(mission).complexity { + Complexity::Simple => LAB_LOOP_CORE, + Complexity::Medium => LAB_LOOP_STANDARD, + Complexity::Complex | Complexity::Epic => LAB_LOOP_STEPS, + } +} + +pub fn lab_plan_spec_for_mission(mission: &str) -> String { + lab_plan_for_mission(mission).join("|") +} + /// Prompt block injected into every dispatched oracle so the Lab loop is /// operational, not a blog post. pub fn oracle_lab_block() -> String { + oracle_lab_block_for_mission("") +} + +/// Mission-scoped Lab block: the persisted plan matches routed complexity. +pub fn oracle_lab_block_for_mission(mission: &str) -> String { format!( "\n## AGK Agentic Engineering Lab (run this, do not narrate it)\n\ Persist this plan first: `omega progress --plan \"{}\"`\n\ - Walk the steps in order. Keep exactly one task `doing`.\n\ + Walk the steps in order. Keep exactly one task `doing`. Do not invent \ + Deploy/Observe steps the plan does not list.\n\ Required coding-agent dimensions on every mission: repo context, editing, \ shell, tests, git, sandbox, verification, human-in-the-loop, finish reports.\n\ Writers (claude|codex|glm) cannot self-approve. `omega done` is a candidate, \ never a verdict. Fake-done is forbidden.\n\ YOU fill R-RUBRIC when you write the worker prompt — Done Criteria AND \ - a Verify Command (a runtime check, not a bare filename to eval). Do not \ - leave that for a human `--force`.\n\ + a Verify Command (a runtime check). There is no auto-fill and no `--force` skip.\n\ `omega spawn-worker \"\\nDone Criteria: \\nVerify Command: \" --dir --files a,b`\n\ - Workers start in that --dir (the project). The parent never evals Verify Command at spawn.\n\ + Workers start in that --dir (the project). The parent never evals Verify Command at spawn; \ + `omega done done_clean` re-runs it.\n\ Workers are claude|codex|glm only. Hermes is Home (`omega new --agent hermes`), \ never dispatch and never a worker.\n", - lab_plan_spec() + lab_plan_spec_for_mission(mission) ) } @@ -52,41 +78,38 @@ pub fn oracle_lab_block() -> String { pub const DONE_CRITERIA_LABEL: &str = "Done Criteria:"; pub const VERIFY_COMMAND_LABEL: &str = "Verify Command:"; +fn has_done_criteria_label(prompt: &str) -> bool { + let lower = prompt.to_lowercase(); + lower.contains("done criteria:") || lower.contains("done-criteria:") +} + /// True when a worker prompt already satisfies the spawn-worker rubric gate. +/// Requires the real labels, not the word "verify" somewhere in the brief. pub fn worker_prompt_has_rubric(prompt: &str) -> bool { - let lower = prompt.to_lowercase(); - let has_done = lower.contains("done criteria") - || lower.contains("done:") - || lower.contains("done-criteria"); - let has_verify = lower.contains("verify"); - has_done && has_verify + has_done_criteria_label(prompt) && crate::worker_spawn::parse_verify_contract(prompt).is_some() } -/// Ensure an oracle-authored brief includes the two R-RUBRIC fields. -/// -/// Human `omega spawn-worker` from a shell still refuses a missing rubric -/// (`--force` does not skip it). Oracle-originated briefs get the fields -/// appended so a worker is not blocked on prompt wording. -pub fn ensure_oracle_worker_rubric(prompt: &str, task: &str) -> String { +/// Oracle briefs are not auto-filled. A missing rubric is a hard error so +/// `{task}.evidence` cannot become a fake green. +pub fn require_worker_rubric(prompt: &str) -> Result<(), String> { if worker_prompt_has_rubric(prompt) { - return prompt.to_string(); - } - let mut out = prompt.trim_end().to_string(); - let lower = out.to_lowercase(); - if !(lower.contains("done criteria") - || lower.contains("done:") - || lower.contains("done-criteria")) - { - out.push_str(&format!( - "\n\n{DONE_CRITERIA_LABEL} task `{task}` is complete, verified by runtime evidence, \ - and reported via `omega done` with a summary. Fake-done is forbidden.\n" - )); + return Ok(()); } - if !out.to_lowercase().contains("verify") { - let artifact = format!("{task}.evidence"); - out.push_str(&format!("\n{VERIFY_COMMAND_LABEL} test -f {artifact}\n")); - } - out + let missing = match ( + has_done_criteria_label(prompt), + crate::worker_spawn::parse_verify_contract(prompt).is_some(), + ) { + (false, false) => "Done Criteria: + Verify Command:", + (false, true) => "Done Criteria:", + (true, false) => { + "a safe Verify Command: (no shell operators; a real runtime check, not a vibe)" + } + (true, true) => unreachable!(), + }; + Err(format!( + "worker prompt missing {missing}. The oracle must write both fields (R-RUBRIC). \ + There is no auto-fill and --force does not skip this." + )) } #[cfg(test)] @@ -113,23 +136,46 @@ mod tests { } #[test] - fn oracle_briefs_gain_rubric_fields_when_missing() { + fn lab_plan_scales_with_mission_complexity() { + assert_eq!( + lab_plan_for_mission("typo in the README"), + LAB_LOOP_CORE, + "a tiny ask must not inherit Deploy/Observe" + ); + assert_eq!( + lab_plan_spec_for_mission("typo in the README"), + "Understand|Build|Verify" + ); + assert_eq!( + lab_plan_for_mission("complete overhaul of the entire system from scratch"), + LAB_LOOP_STEPS + ); + } + + #[test] + fn missing_rubric_is_a_hard_error_not_an_autofill() { let raw = "implement orch test file"; assert!(!worker_prompt_has_rubric(raw)); - let filled = ensure_oracle_worker_rubric(raw, "orch-test"); - assert!(worker_prompt_has_rubric(&filled), "{filled}"); - assert!(filled.contains("Done Criteria:")); - assert!(filled.contains("Verify Command:")); + let err = require_worker_rubric(raw).expect_err("auto-fill is forbidden"); + assert!(err.contains("R-RUBRIC"), "{err}"); assert!( - filled.contains("test -f orch-test.evidence"), - "auto-fill must be a runtime check, not a bare filename to eval: {filled}" + !raw.contains("orch-test.evidence"), + "must not invent a fake evidence file" ); } #[test] - fn complete_briefs_are_left_alone() { - let raw = "Write ORCH_TEST.txt\nDone Criteria: file exists\nVerify Command: test -f ORCH_TEST.txt"; + fn the_word_verify_alone_is_not_a_rubric() { + let raw = "please verify the auth fix\nDone: looks good"; + assert!(!worker_prompt_has_rubric(raw)); + assert!(require_worker_rubric(raw).is_err()); + } + + #[test] + fn complete_briefs_are_accepted() { + let raw = + "Write ORCH_TEST.txt\nDone Criteria: file exists\nVerify Command: test -f ORCH_TEST.txt"; assert!(worker_prompt_has_rubric(raw)); - assert_eq!(ensure_oracle_worker_rubric(raw, "t"), raw); + assert!(require_worker_rubric(raw).is_ok()); } } diff --git a/crates/omega-core/src/oracle_lifecycle.rs b/crates/omega-core/src/oracle_lifecycle.rs index b04c19e5..af3438ba 100644 --- a/crates/omega-core/src/oracle_lifecycle.rs +++ b/crates/omega-core/src/oracle_lifecycle.rs @@ -1334,7 +1334,7 @@ impl OraclePromptGenerator { // same generic advice and both defaulted to doing the work themselves // instead of spawning and supervising workers. prompt.push_str(&crate::mission_patterns::orchestration_block(mission)); - prompt.push_str(&crate::lab::oracle_lab_block()); + prompt.push_str(&crate::lab::oracle_lab_block_for_mission(mission)); prompt.push_str("\n---\n\n"); // Layer 2 — the shared v2 identity/protocol template. diff --git a/crates/omega-tui/src/app.rs b/crates/omega-tui/src/app.rs index 5b305e86..8331de66 100644 --- a/crates/omega-tui/src/app.rs +++ b/crates/omega-tui/src/app.rs @@ -1191,7 +1191,6 @@ pub enum MonitorAction { TelegramDisconnect, ProvisioningSetup, RefreshBilling, - OpenDashboard, UpdateOmega, } @@ -1203,7 +1202,6 @@ impl MonitorAction { MonitorAction::TelegramDisconnect, MonitorAction::ProvisioningSetup, MonitorAction::RefreshBilling, - MonitorAction::OpenDashboard, MonitorAction::UpdateOmega, ] } @@ -1214,7 +1212,6 @@ impl MonitorAction { MonitorAction::TelegramDisconnect => "Disconnect Telegram bot", MonitorAction::ProvisioningSetup => "Set up project provisioning keys (Vercel/Convex/GitHub/Stripe)", MonitorAction::RefreshBilling => "Refresh billing now (live OAuth usage check)", - MonitorAction::OpenDashboard => "Open Dashboard (OmegaMC Telegram dashboard — replaces aisb-master)", MonitorAction::UpdateOmega => "Update OmegaOS now (pull + rebuild + reinstall — your ~/.omega state is preserved)", } } @@ -1225,74 +1222,9 @@ impl MonitorAction { MonitorAction::TelegramDisconnect => "D", MonitorAction::ProvisioningSetup => "P", MonitorAction::RefreshBilling => "B", - MonitorAction::OpenDashboard => "O", MonitorAction::UpdateOmega => "U", } } - /// Resolve the "Open Dashboard" action against the real filesystem. - /// - /// OmegaMC (the Telegram-controlled web dashboard, `agentik-os/agentik-telegram`) - /// is installed by `install.sh` Phase 6.95 into `$OMEGA_DIR/repos/omega-mc` — a - /// best-effort clone that may be absent (private repo / `OMEGA_SKIP_DASHBOARD=1`). - /// - /// Present → launch through `omega-mc-up` (the caller turns this into - /// `Action::RunShellCommand`, the same mechanism the Settings - /// install/uninstall actions use). A raw `docker compose up -d` is NOT - /// enough: omega-mc-up first generates `.env` from live OmegaOS state (bot - /// token, Claude OAuth, DOCKER_GID, one-time vault passphrase), ensures - /// config/omega-mc.yaml, and builds the three LOCAL images that are never - /// published to GHCR — on a fresh install compose alone fails on the - /// missing .env/images. It's idempotent, so it's also the right re-launch - /// path. Absent → return the honest install instructions. - pub fn resolve_open_dashboard() -> DashboardLaunch { - let dir = omega_core::config::omega_dir() - .join("repos") - .join("omega-mc"); - let dir_str = dir.to_string_lossy().to_string(); - // The directory alone isn't proof of a usable clone; require the .git - // marker install.sh checks (a failed clone is `rm -rf`'d, but a partial - // manual copy could leave a bare dir). Runtime truth over assumption. - if dir.join(".git").is_dir() { - // install.sh symlinks omega-mc-up onto PATH; fall back to the - // installed copy in $OMEGA_DIR/bin for shells that miss the link. - let fallback = omega_core::config::omega_dir() - .join("bin") - .join("omega-mc-up.sh"); - DashboardLaunch::Launch { - command: format!( - "echo '── Starting OmegaMC dashboard (omega-mc-up) ──' && {{ command -v omega-mc-up >/dev/null 2>&1 && omega-mc-up || {fb}; }} && echo && echo 'Dashboard up. Local URL: http://localhost:8080 (see {dir}/docker-compose.yml for the published port; AISB agents in config/omega-aisb.yaml).'", - fb = shell_quote(&fallback.to_string_lossy()), - dir = shell_quote(&dir_str), - ), - message: format!( - "▶ Starting OmegaMC dashboard via omega-mc-up ({dir_str}) — watch the spawned session; URL printed there once containers are up." - ), - } - } else { - DashboardLaunch::NotInstalled { - message: format!( - "OmegaMC dashboard not installed. Install it with: git clone https://github.com/agentik-os/agentik-telegram.git {dir_str} && omega-mc-up" - ), - } - } - } -} - -/// Result of resolving the Monitor "Open Dashboard" action against the -/// filesystem. Mapped to an `Action` by the input layer (kept Action-free here -/// so `app.rs` stays decoupled from `input.rs`). -#[derive(Debug, Clone)] -pub enum DashboardLaunch { - /// OmegaMC is installed — launch it in a session via the given shell command. - Launch { command: String, message: String }, - /// OmegaMC is absent — show honest install instructions, no command run. - NotInstalled { message: String }, -} - -/// Single-quote a string for safe interpolation into a `bash -c` command. Wraps -/// in single quotes and escapes embedded single quotes the POSIX way (`'\''`). -fn shell_quote(s: &str) -> String { - format!("'{}'", s.replace('\'', "'\\''")) } pub struct App { diff --git a/crates/omega-tui/src/input.rs b/crates/omega-tui/src/input.rs index f8f10708..1cb43bc3 100644 --- a/crates/omega-tui/src/input.rs +++ b/crates/omega-tui/src/input.rs @@ -1749,12 +1749,7 @@ fn handle_key_normal(app: &mut App, key: KeyEvent) -> Action { match app.selected_monitor_section() { MonitorSection::Actions => { let action = app.selected_monitor_action(); - // OpenDashboard needs `&mut App` → its own handler. - if matches!(action, MonitorAction::OpenDashboard) { - open_dashboard_action(app) - } else { - execute_monitor_action(action) - } + execute_monitor_action(action) } // Account & billing → the OAuth re-login engine. Context- // aware: once the authorize URL is captured, Enter opens @@ -2014,9 +2009,6 @@ Statut actuel: {status}.", KeyCode::Char('B') if app.tab == Tab::Settings && app.settings_on_monitor() => { Action::RefreshBilling } - KeyCode::Char('O') if app.tab == Tab::Settings && app.settings_on_monitor() => { - open_dashboard_action(app) - } KeyCode::Char('U') if app.tab == Tab::Settings && app.settings_on_monitor() => { execute_monitor_action(MonitorAction::UpdateOmega) } @@ -2430,11 +2422,6 @@ fn execute_monitor_action(action: MonitorAction) -> Action { MonitorAction::TelegramDisconnect => Action::TelegramDisconnect, MonitorAction::ProvisioningSetup => Action::ProvisioningSetup, MonitorAction::RefreshBilling => Action::RefreshBilling, - // OpenDashboard needs the `&mut App` to surface the honest "not - // installed" status when OmegaMC is absent, so the Enter/letter - // handlers route through `open_dashboard_action(app)` directly. This - // arm is unreachable in practice but keeps the match total. - MonitorAction::OpenDashboard => Action::None, // Update runs as a DETACHED session (same reason the General-section // update does): `omega update` rebuilds the very binary this TUI runs // from and the build takes minutes, so spawning it keeps the UI alive @@ -2447,28 +2434,6 @@ fn execute_monitor_action(action: MonitorAction) -> Action { } } -/// Resolve + dispatch the Monitor "Open Dashboard" action. When OmegaMC is -/// installed (`$OMEGA_DIR/repos/omega-mc/.git`), launch it via -/// `Action::RunShellCommand` (`omega-mc-up` — see `resolve_open_dashboard` -/// for why raw compose isn't enough) — the same session-spawning mechanism -/// the Settings install/uninstall actions use. When absent, set an honest -/// install message and dispatch nothing. -fn open_dashboard_action(app: &mut App) -> Action { - match MonitorAction::resolve_open_dashboard() { - crate::app::DashboardLaunch::Launch { command, message } => { - app.status_message = Some(message); - Action::RunShellCommand { - label: "OmegaMC dashboard".to_string(), - command, - } - } - crate::app::DashboardLaunch::NotInstalled { message } => { - app.status_message = Some(message); - Action::None - } - } -} - /// Chat-input mode — REAL-TIME keystroke passthrough to the streamed rmux /// session. Every key (printable, Enter, Backspace, arrows, Ctrl-combos) /// is forwarded one-by-one so plan mode, OAuth code paste, and choice diff --git a/crates/omega-tui/src/ui.rs b/crates/omega-tui/src/ui.rs index 016c68d3..3aed585d 100644 --- a/crates/omega-tui/src/ui.rs +++ b/crates/omega-tui/src/ui.rs @@ -908,7 +908,7 @@ fn draw_project_delete_picker(frame: &mut Frame, app: &App) { _ => return, }; let options = [ - "1. Remove from OmegaOS — topic + dashboard agent + agent-bot + registry (folder & GitHub kept)", + "1. Remove from OmegaOS — topic + agent-bot + registry (folder & GitHub kept)", "2. Delete local machine — that + kill oracle + DELETE the local folder (GitHub kept)", "3. Delete ALL (+ GitHub) — that + DELETE the GitHub repo (nothing remains)", " Cancel", @@ -2470,11 +2470,11 @@ fn render_monitor_telegram_from(tg_config: TelegramConfigRead) -> Vec Vec> { Line::from(" photos (transcribed + analysed). Per-project topics on sync."), Line::from(""), Line::from(Span::styled( - " OmegaMC dashboard & gateway", - Style::default().fg(th::accent()), - )), - Line::from(" The phone-side web control surface (agents, conversations, tasks,"), - Line::from(" swarms) backed by the on-demand gateway. Repo: agentik-os/"), - Line::from(" agentik-telegram (MIT); runs as a Docker container on :8080."), - Line::from(" Open: Settings tab → Actions → 'O' (Open Dashboard) — launches it"), - Line::from(" from ~/.omega/repos/omega-mc when installed."), - Line::from(" The 15 AISB agents are mapped into its registry"), - Line::from(" (config/omega-aisb.yaml)."), - Line::from(""), - Line::from(Span::styled( - " One brain (Atlas), one Telegram channel, one dashboard — all 15 agents.", + " One brain (Atlas), one Telegram channel — all 15 AISB agents.", Style::default().fg(th::dim()), )), ] @@ -4392,12 +4380,12 @@ fn render_info_oracle() -> Vec> { Line::from(""), Line::from(" An Oracle is spawned by `omega dispatch \"\"`."), Line::from(" Its job is to:"), - Line::from(" 1. CLASSIFY the mission's complexity (SIMPLE / MEDIUM / COMPLEX / EPIC)"), - Line::from(" 2. PLAN — if COMPLEX or EPIC, KEYMAKER decomposes into a DAG"), - Line::from(" 3. DISPATCH workers via rmux sessions (1 per task)"), - Line::from(" 4. MONITOR — wait for each worker's done.json"), - Line::from(" 5. VERIFY — run the quality gate (rubric + multi-grader + adversarial)"), - Line::from(" 6. REPORT — write its own done.json with the outcome summary"), + Line::from(" 1. CLASSIFY complexity (SIMPLE / MEDIUM / COMPLEX / EPIC)"), + Line::from(" 2. SCALE the Lab plan — a typo is Understand|Build|Verify, not Deploy"), + Line::from(" 3. WRITE a real R-RUBRIC into every worker brief (no auto-fill)"), + Line::from(" 4. DISPATCH writers (claude|codex|glm) via rmux, file-scope locked"), + Line::from(" 5. TREAT worker done.json as a candidate — re-run Verify Command"), + Line::from(" 6. REPORT — oracle done.json is still not self-approval"), Line::from(""), Line::from(Span::styled(" Naming", Style::default().fg(th::accent()))), Line::from(" Sessions: oracle- (1st)"), @@ -4407,13 +4395,13 @@ fn render_info_oracle() -> Vec> { " Rules enforced", Style::default().fg(th::accent()), )), - Line::from(" R-19 — Rubric before execution"), - Line::from(" R-21 — Multi-grader consensus ≥ 2/3"), - Line::from(" R-28 — Token budget enforced (default 500K)"), - Line::from(" L3 — Workers must decide, not wait"), + Line::from(" R-RUBRIC — measurable Done Criteria + Verify Command before spawn"), + Line::from(" R-VERIFY — a delegate's done is an input, never the verdict"), + Line::from(" R-BUDGET — 500K token cap; escalate, do not silently overrun"), + Line::from(" L3 — dispatched sessions decide and proceed"), Line::from(""), Line::from(Span::styled( - " ORACLES NEVER write code — they decide who does, then verify.", + " Oracles supervise. Writers cannot self-approve. Fake-done is forbidden.", Style::default().fg(th::dim()), )), ] @@ -4429,9 +4417,9 @@ fn render_info_workers() -> Vec> { .add_modifier(Modifier::BOLD), )), Line::from(""), - Line::from(" A worker is one rmux session running an agent (usually Claude) with"), - Line::from(" a specific task prompt. Workers are short-lived: spawned by an Oracle,"), - Line::from(" they execute one task, signal done, and the patrol cleans up."), + Line::from(" A worker is one rmux session running a writer (claude, codex, or glm)"), + Line::from(" with a specific task prompt. Workers are short-lived: spawned by an"), + Line::from(" Oracle, they execute one task, signal done, and the patrol cleans up."), Line::from(""), Line::from(Span::styled(" Naming", Style::default().fg(th::accent()))), Line::from(" -worker- e.g. Causio-worker-auth"), @@ -4440,20 +4428,20 @@ fn render_info_workers() -> Vec> { " Lifecycle", Style::default().fg(th::accent()), )), - Line::from(" 1. Oracle spawns: omega spawn-worker auth \"\" --project Causio"), - Line::from(" 2. Scope-claim: files_owned locked in ~/.omega/state/scope-*.json"), - Line::from(" 3. Worker runs: Claude (or other agent) executes the task"), - Line::from(" 4. Worker reports: omega done done_clean \"\""), - Line::from(" 5. Patrol acks: omega patrol --once releases the scope claim"), - Line::from(" 6. Quality gate: Oracle's rubric is graded against the result"), + Line::from(" 1. Oracle writes Done Criteria + Verify Command (R-RUBRIC) — no auto-fill"), + Line::from(" 2. Spawn: omega spawn-worker auth \"\" --dir --files a,b"), + Line::from(" 3. Scope-claim: files_owned locked in ~/.omega/state/scope-*.json"), + Line::from(" 4. Worker starts in the project dir (never rmux `.` / $HOME)"), + Line::from(" 5. omega done done_clean re-runs the Verify Command"), + Line::from(" 6. Patrol acks; the oracle still verifies (R-VERIFY)"), Line::from(""), Line::from(Span::styled( " Rules enforced", Style::default().fg(th::accent()), )), - Line::from(" L3 — autonomy: decide, never wait"), + Line::from(" L1 / L4 — runtime evidence; done means 100%"), + Line::from(" R-VERIFY — worker done.json is a candidate, not a verdict"), Line::from(" SCOPE-CLAIM — no two workers may edit the same file"), - Line::from(" R-18 — long-running missions go here, short go to Agent tool"), Line::from(""), Line::from(" Workers can run in PARALLEL when their file scopes are disjoint."), Line::from(" When they overlap, the dispatcher serializes them automatically."), @@ -5218,7 +5206,6 @@ fn draw_help(frame: &mut Frame, app: &mut App, area: Rect) { key("T / D", "Telegram setup / disconnect"), key("P", "Set up provisioning keys"), key("B", "Refresh billing"), - key("O", "Open OmegaMC dashboard"), key("U", "Update OmegaOS"), Line::from(""), section("Settings"), diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index a68dad7c..0ce2ee87 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -83,7 +83,7 @@ sessions through a unified configuration and orchestration layer. │ ├── audit-gather/ per-audit gatherers (.sh + -summarize.py) │ └── safe-npm-build.sh build mutex ├── bin/ audit-notify.sh + helper binaries -├── repos/ ← cloned GitHub repos (repos/omega-mc = dashboard) +├── repos/ ← cloned GitHub repos (repos/Agentik-Skills = skills library) ├── tools/ ← third-party tools an agent installs ├── prompts/ runtime prompt scratch (oracle/worker dispatch) │ diff --git a/docs/GETTING-STARTED.md b/docs/GETTING-STARTED.md index 995d0345..84dbcd42 100644 --- a/docs/GETTING-STARTED.md +++ b/docs/GETTING-STARTED.md @@ -147,8 +147,6 @@ local chat), and `omega attach -t ` (jump into any live agent). ## Optional extras -- **Mission Control dashboard** (web UI, one container per agent — needs - Docker): `omega-mc-up`, then open `http://:8080`. - **More CLI agents**: `omega install claude|antigravity|gemini|openrouter|pi|hermes|glm|kimi` (or diff --git a/install.sh b/install.sh index 6bbc89bd..3c86f99f 100755 --- a/install.sh +++ b/install.sh @@ -138,7 +138,7 @@ omega_sudo() { # Portable timeout: GNU coreutils `timeout` is guaranteed on Linux; macOS only # grew a native /usr/bin/timeout in macOS 13, and older boxes have at best # brew's `gtimeout`. Exit code 127 from a missing binary silently disabled the -# claude auto-install + the Agentik-Skills/OmegaMC clones on such Macs (the +# claude auto-install + the Agentik-Skills clone on such Macs (the # `||`/`elif` guards turned it into misleading "no auth?" skip messages). # Fallback: run un-timed — strictly better than not running at all (git is # already prompt-proofed via GIT_TERMINAL_PROMPT=0, so clones fail fast). @@ -1290,7 +1290,7 @@ fi step "Phase 5: Configuring OmegaOS" # Single OmegaOS home. Defined sub-dirs so future installs land in the RIGHT -# place (see docs/ARCHITECTURE.md): repos/ = cloned github repos (e.g. omega-mc), +# place (see docs/ARCHITECTURE.md): repos/ = cloned github repos (e.g. Agentik-Skills), # tools/ = third-party tools/binaries an agent installs, prompts/ = runtime # prompt scratch, lib/ + bin/ = audit runtime. No dual ~/.aisb home. mkdir -p "$OMEGA_DIR"/{state,logs,locks,repos,tools,prompts,lib,bin} @@ -3673,23 +3673,13 @@ else info "Nova personal-assistant layer deferred (needs its own bot token + NOVA_CHAT_ID). Add: OMEGA_WITH_NOVA=1 ./install.sh" fi -# ─── Phase 6.95: Telegram interface → OmegaMC (Agentik-Telegram, Go + Docker) ─── -# OmegaMC (agentik-os/agentik-telegram) is the OmegaOS Telegram control plane: it -# routes Telegram messages to named AISB agents — each running Claude Code in its -# own Docker container — with @agent routing + a Mission Control web UI. This is -# the "talk to your agents from your phone" layer (RESTORES the powerful Go+Docker -# OmegaMC; supersedes the interim single-session Bun bot). The heavy image builds -# (chromium + embedding model) are DEFERRED to connect-time, exactly like the -# browser stack: cloning is cheap; `omega-mc-up` builds the three local images and -# `docker compose up -d` on demand (the images are not published to GHCR because -# the agent image bundles the Claude Code binary, so we build from source). The -# bring-up reads the bot token from ~/.omega/telegram.toml and the Claude OAuth -# token from ~/.omega/credentials/claude.json. Skip entirely: OMEGA_SKIP_DASHBOARD=1. +# ─── Phase 6.95: Agentik-Skills library (canonical SSOT) ─── # Agentik-Skills — the canonical SSOT skills library (agentik-os/Agentik-Skills). # Skills here mirror into ~/.omega/skills// and `omega sync` symlinks them # into every LLM (Claude ~/.claude/skills, Gemini/Codex via OMEGA.md). Private repo -# → needs gh/git auth; best-effort, like the dashboard. -step "Phase 6.95: Telegram interface (OmegaMC)" +# → needs gh/git auth; best-effort. Telegram phone control is the command bot +# (omega-tg-up), not a separate web dashboard. +step "Phase 6.95: Agentik-Skills library" SKILLS_REPO_DIR="$OMEGA_DIR/repos/Agentik-Skills" mkdir -p "$OMEGA_DIR/repos" if [[ -d "$SKILLS_REPO_DIR/.git" ]]; then @@ -3852,37 +3842,11 @@ if [[ -x "$INSTALL_DIR/omega" ]]; then || info "post-mirror provider sync had warnings (re-run: omega sync)" fi -if [[ "${OMEGA_SKIP_DASHBOARD:-0}" != "1" ]]; then - MC_DIR="$OMEGA_DIR/repos/omega-mc" - mkdir -p "$OMEGA_DIR/repos" - if [[ -d "$MC_DIR/.git" ]]; then - ok "OmegaMC present ($MC_DIR — update: git -C $MC_DIR pull && omega-mc-up --rebuild)" - elif omega_timeout 120 git clone --depth 1 https://github.com/agentik-os/agentik-telegram.git "$MC_DIR" >/dev/null 2>&1; then - ok "OmegaMC cloned → $MC_DIR" - else - rm -rf "$MC_DIR" 2>/dev/null || true - info "OmegaMC clone skipped — repo unreachable (no git auth?). Later: git clone https://github.com/agentik-os/agentik-telegram.git $MC_DIR" - fi - # Ship the bring-up helper (generates .env from OmegaOS state → builds images - # → docker compose up) onto the omega bin PATH. - if [[ -f "$OMEGA_SRC/scripts/omega-mc-up.sh" ]]; then - cp -f "$OMEGA_SRC/scripts/omega-mc-up.sh" "$OMEGA_DIR/bin/omega-mc-up.sh" - chmod +x "$OMEGA_DIR/bin/omega-mc-up.sh" - # symlink onto PATH ($INSTALL_DIR is added to PATH above) so `omega-mc-up` just works - ln -sf "$OMEGA_DIR/bin/omega-mc-up.sh" "$INSTALL_DIR/omega-mc-up" 2>/dev/null || true - fi - # Seed the AISB 13-agent roster as the active config if absent. - if [[ -d "$MC_DIR/config" && ! -f "$MC_DIR/config/omega-mc.yaml" && -f "$MC_DIR/config/omega-aisb.yaml" ]]; then - cp "$MC_DIR/config/omega-aisb.yaml" "$MC_DIR/config/omega-mc.yaml" - fi - if [[ -d "$MC_DIR/.git" ]]; then - # OmegaMC is the OPTIONAL multi-agent backend (Claude-per-container + Mission - # Control web UI). It is NOT auto-started: it would poll the same bot token as - # the command bot (Phase c), and Telegram allows ONE poller per token. Run it - # on a SEPARATE bot token, or stop omega-tg-bot first, then: omega-mc-up. - info "OmegaMC (optional multi-agent backend) installed → $MC_DIR. It needs its OWN bot token (don't share the command bot's). Bring up the dashboard (no token needed — the command bot owns Telegram): omega-mc-up (needs Docker)" - fi -fi +# Retired Mission Control web UI (separate Docker stack). Telegram stays the +# phone control plane via omega-tg-up. Drop leftover launchers so a previous +# install cannot start the dead stack. Do not delete ~/.omega/repos/omega-mc — +# that tree may hold a .env with tokens. +rm -f "$OMEGA_DIR/bin/omega-mc-up.sh" "$INSTALL_DIR/omega-mc-up" 2>/dev/null || true # ─── Done ───────────────────────────────────────────────────────────────────── diff --git a/scripts/omega-mc-up.sh b/scripts/omega-mc-up.sh deleted file mode 100755 index 86f28e92..00000000 --- a/scripts/omega-mc-up.sh +++ /dev/null @@ -1,178 +0,0 @@ -#!/usr/bin/env bash -# ═══════════════════════════════════════════════════════════════════════════ -# omega-mc-up.sh — bring up the OmegaMC / Agentik-Telegram control plane -# ─────────────────────────────────────────────────────────────────────────── -# OmegaMC (agentik-os/agentik-telegram, Go + Docker) is the OmegaOS Telegram -# interface: it routes Telegram messages to named AISB agents, each running -# Claude Code in its own container, with @agent routing + a Mission Control -# web UI. This script makes "connect Telegram → it works" a single command: -# -# 1. Generate repos/omega-mc/.env from live OmegaOS state -# (bot token ← ~/.omega/telegram.toml, Claude OAuth ← credentials/claude.json, -# DOCKER_GID ← the host docker group). Vault passphrase + web password are -# generated ONCE and preserved across runs. -# 2. Ensure config/omega-mc.yaml exists (AISB 14-agent roster). -# 3. Build the three local images if missing (gateway, agent-base, agent) — -# they are NOT published to GHCR (the agent image bundles the Claude Code -# binary, which can't be redistributed), so we build from source. -# 4. docker compose up -d (restart: unless-stopped → survives reboot). -# -# Idempotent: safe to re-run to refresh the Claude OAuth token or after edits. -# Usage: omega-mc-up.sh [--rebuild] [ []] -# --rebuild force image rebuilds -# one-shot connect: write them to telegram.toml, then up -# ═══════════════════════════════════════════════════════════════════════════ -set -euo pipefail - -# Some launch contexts (npx-spawned install, `docker exec`) have no USER var; set -# -u would then abort on every "$USER". Default it from the real uid. -: "${USER:=$(id -un)}" - -OMEGA_DIR="${OMEGA_DIR:-$HOME/.omega}" -MC_DIR="${OMEGA_MC_DIR:-$OMEGA_DIR/repos/omega-mc}" -TG_TOML="$OMEGA_DIR/telegram.toml" -CLAUDE_CREDS="$OMEGA_DIR/credentials/claude.json" -GW_IMAGE="ghcr.io/agentik-os/omega-mission-control:latest" -BASE_IMAGE="ghcr.io/agentik-os/omega-mission-control-agent-base:latest" -AGENT_IMAGE="omega-mc-agent:latest" -REBUILD=0; [[ "${1:-}" == "--rebuild" ]] && { REBUILD=1; shift; } -ARG_TOKEN="${1:-}"; ARG_CHAT="${2:-}" - -die() { echo "omega-mc-up: $*" >&2; exit 1; } - -# One-shot connect: persist a freshly-supplied bot token + chat id to telegram.toml -# (validated against Telegram getMe so we fail loudly on a dead token). -if [[ -n "$ARG_TOKEN" ]]; then - if command -v curl >/dev/null 2>&1; then - curl -sf "https://api.telegram.org/bot${ARG_TOKEN}/getMe" >/dev/null 2>&1 \ - || die "bot token rejected by Telegram (getMe 401) — get a fresh token from @BotFather" - fi - umask 077 - cat > "$TG_TOML" </dev/null 2>&1 && SUDO="sudo" -ensure_docker() { - if ! command -v docker >/dev/null 2>&1; then - echo "omega-mc-up: Docker not found — installing (get.docker.com)…" - if command -v curl >/dev/null 2>&1; then curl -fsSL https://get.docker.com | $SUDO sh >/dev/null 2>&1 || true; fi - command -v docker >/dev/null 2>&1 || die "Docker install failed — install Docker manually, then re-run" - fi - # daemon up (systemd or sysv); harmless if already running - $SUDO systemctl enable --now docker >/dev/null 2>&1 || $SUDO service docker start >/dev/null 2>&1 || true - # current user in the docker group (applies to NEW logins; this run uses sg/sudo) - if [[ $EUID -ne 0 ]] && ! id -nG "$USER" | grep -qw docker; then - getent group docker >/dev/null 2>&1 || $SUDO groupadd docker >/dev/null 2>&1 || true - $SUDO usermod -aG docker "$USER" >/dev/null 2>&1 \ - && echo "omega-mc-up: added $USER to the docker group (log out/in for it to apply to your shell)" - fi -} -ensure_docker - -# Pick a docker invocation that actually reaches the daemon in THIS process: -# direct (group already active / root) → sg docker (group set but not in this -# login) → sudo (last resort). -DOCK_MODE=direct -if ! docker version >/dev/null 2>&1; then - if command -v sg >/dev/null 2>&1 && id -nG "$USER" | grep -qw docker && sg docker -c "docker version" >/dev/null 2>&1; then - DOCK_MODE=sg - elif $SUDO docker version >/dev/null 2>&1; then - DOCK_MODE=sudo - fi -fi -dock() { - case "$DOCK_MODE" in - sg) sg docker -c "docker $(printf '%q ' "$@")" ;; - sudo) $SUDO docker "$@" ;; - *) docker "$@" ;; - esac -} -dock version >/dev/null 2>&1 || die "cannot reach the Docker daemon (tried direct, sg docker, sudo)" - -# ── 1. Gather state ──────────────────────────────────────────────────────── -# OmegaMC runs in DASHBOARD mode: Mission Control web UI + the 15 AISB agents, -# with the in-gateway Telegram bot DISABLED (token left empty) so it never -# 409-conflicts with the OmegaOS command bot, which owns the bot token. No -# telegram.toml is required here. -ALLOW_ID=$(grep -oP 'allow_user_ids\s*=\s*\[\s*\K[0-9]+' "$TG_TOML" 2>/dev/null || true) -CHAT_ID=$(grep -oP 'chat_id\s*=\s*\K-?[0-9]+' "$TG_TOML" 2>/dev/null || true) -OPERATOR="${ALLOW_ID:-$CHAT_ID}" - -OAUTH="" -if [[ -f "$CLAUDE_CREDS" ]] && command -v jq >/dev/null 2>&1; then - OAUTH=$(jq -r '.claudeAiOauth.accessToken // empty' "$CLAUDE_CREDS" 2>/dev/null || true) -fi -[[ -n "$OAUTH" || -n "${ANTHROPIC_API_KEY:-}" ]] || \ - echo "omega-mc-up: WARNING — no Claude OAuth token in $CLAUDE_CREDS and no ANTHROPIC_API_KEY; agents will fail auth until one is set." >&2 - -DGID=$(getent group docker | cut -d: -f3 || echo 988) - -# ── 2. Generate .env (preserve generated secrets across runs) ────────────── -ENV_FILE="$MC_DIR/.env" -prev() { [[ -f "$ENV_FILE" ]] && grep -oP "^$1=\K.*" "$ENV_FILE" 2>/dev/null | head -1 || true; } -VAULT=$(prev OMEGA_MC_VAULT_PASSPHRASE); [[ -n "$VAULT" ]] || VAULT=$(openssl rand -hex 24) -WEBPW=$(prev OMEGA_MC_WEB_PASSWORD); [[ -n "$WEBPW" ]] || WEBPW=$(openssl rand -hex 12) - -umask 077 -cat > "$ENV_FILE" </dev/null 2>&1; } -cd "$MC_DIR" -if [[ $REBUILD -eq 1 ]] || ! have "$BASE_IMAGE"; then - echo "omega-mc-up: building agent-base (chromium + nix + embedding model — several minutes)…" - dock build -f Dockerfile.agent-base -t "$BASE_IMAGE" . -fi -if [[ $REBUILD -eq 1 ]] || ! have "$AGENT_IMAGE"; then - echo "omega-mc-up: building agent image…" - dock compose build agent -fi -if [[ $REBUILD -eq 1 ]] || ! have "$GW_IMAGE"; then - echo "omega-mc-up: building gateway image…" - dock build -f Dockerfile -t "$GW_IMAGE" . -fi - -# ── 5. Up ─────────────────────────────────────────────────────────────────── -echo "omega-mc-up: starting stack…" -dock compose up -d -echo "omega-mc-up: ✓ OmegaMC up. Mission Control → http://localhost:8080 (logs: docker compose -f $MC_DIR/docker-compose.yml logs -f)" diff --git a/scripts/omega-token-refresh.sh b/scripts/omega-token-refresh.sh index 98bdf21c..8144976e 100755 --- a/scripts/omega-token-refresh.sh +++ b/scripts/omega-token-refresh.sh @@ -22,30 +22,6 @@ exp=$(python3 -c "import json,sys;d=json.load(open('$CRED'));o=d.get('claudeAiOa [ -z "${exp:-}" ] && exit 0 now=$(date +%s); left=$((exp - now)) -# ── omega-mc gateway token sync ────────────────────────────────────────────── -# The Mission-Control gateway gets CLAUDE_CODE_OAUTH_TOKEN from its .env at -# container-create time (baked env). Every /login or refresh ROTATES the access -# token, leaving the gateway (and every agent it spawns) with a dead token → -# dashboard agents show "Not logged in • Please run /login". Reconcile on every -# cron pass: if the live token differs from .env, rewrite it and recreate the -# gateway so the fleet follows the credential, hands-free. -MC_DIR="$OMEGA_DIR/repos/omega-mc" -sync_mc() { - [ -f "$MC_DIR/.env" ] || return 0 - local live cur - live=$(python3 -c "import json;d=json.load(open('$CRED'));print(d.get('claudeAiOauth',d).get('accessToken',''))" 2>/dev/null) || return 0 - [ -n "$live" ] || return 0 - cur=$(grep -E '^CLAUDE_CODE_OAUTH_TOKEN=' "$MC_DIR/.env" 2>/dev/null | head -1 | cut -d= -f2-) - [ "$cur" = "$live" ] && return 0 - sed -i "s|^CLAUDE_CODE_OAUTH_TOKEN=.*|CLAUDE_CODE_OAUTH_TOKEN=$live|" "$MC_DIR/.env" - if (cd "$MC_DIR" && sudo -n docker compose up -d --no-build >/dev/null 2>&1); then - echo "$(stamp) omega-mc: token rotated → .env synced + gateway recreated" - else - echo "$(stamp) omega-mc: .env synced (gateway recreate failed — retry next run)" - fi -} -sync_mc - if [ "$left" -ge "$THRESHOLD" ]; then echo "$(stamp) token healthy (${left}s left) — no action" exit 0 @@ -57,7 +33,6 @@ echo "$(stamp) try-refresh: $res" if echo "$res" | grep -q '"ok"[[:space:]]*:[[:space:]]*true'; then echo "$(stamp) refresh OK" - sync_mc # the refresh just rotated the token → follow it into omega-mc now exit 0 fi diff --git a/scripts/verify-install.sh b/scripts/verify-install.sh index e0815a9c..3cf381c8 100755 --- a/scripts/verify-install.sh +++ b/scripts/verify-install.sh @@ -189,7 +189,16 @@ if grep -q "function fanout" telegram-bot/inbox-bot.ts && grep -q "SECRETISH" te # have to remember a tag for a file to be reachable), and the boxes must be hard # links so N boxes cost one file's disk, not N. if grep -q "function boxesFor" telegram-bot/inbox-bot.ts && grep -q "linkSync" telegram-bot/inbox-bot.ts; then ok "deposit boxes wired (untagged → all boxes, hard-linked)"; else bad "deposit named boxes missing/not hard-linked in inbox-bot.ts"; fi -if [ -f scripts/omega-mc-up.sh ] && grep -q "omega-mc-up.sh" install.sh && grep -q "agentik-telegram" install.sh; then ok "OmegaMC optional multi-agent backend shipped + wired (omega-mc-up + agentik-telegram clone)"; else bad "OmegaMC (omega-mc-up.sh / agentik-telegram clone) not shipped/wired in install.sh"; fi +# Mission Control (separate Docker web UI) is retired. Telegram command bot stays. +if [ -f scripts/omega-mc-up.sh ]; then + bad "retired omega-mc-up.sh still in the repo" +elif grep -qE 'git clone.*agentik-telegram' install.sh; then + bad "install.sh still clones the retired Mission Control repo" +elif grep -qE 'cp -f .*omega-mc-up' install.sh; then + bad "install.sh still copies the retired Mission Control launcher" +else + ok "OmegaMC retired (no launcher, no clone, no copy — Telegram command bot remains)" +fi if ls scripts/hooks/*.sh >/dev/null 2>&1 && grep -q "scripts/hooks" install.sh; then ok "tracking + verify hooks shipped + installed"; else bad "hooks not shipped/wired"; fi if [ -f agents/identity/SOUL.template.md ] && grep -q "SOUL.template" install.sh; then ok "SOUL identity template shipped + installed"; else bad "SOUL template not shipped/wired"; fi if grep -q "usage --check" install.sh; then ok "native billing cron (omega usage --check) scheduled"; else bad "native billing cron missing from install.sh"; fi diff --git a/skills/cleanup/SKILL.md b/skills/cleanup/SKILL.md index 22318d09..38689e52 100644 --- a/skills/cleanup/SKILL.md +++ b/skills/cleanup/SKILL.md @@ -42,7 +42,7 @@ claude/rmux. - Sessions / sockets actifs : `claude-*`, `rmux-*`, `tmux-*`, `rx-socketna-*`, `cloudflared`, la worktree d'un build en cours. ## Déroulé -0. **Fermeture des tunnels/mosh détachés** (si demandé) : `scripts/close-sessions.sh` ferme les tunnels cloudflared + serveurs mosh détachés de `vibe`/`lab` — et RIEN d'autre : claude et rmux sont hors périmètre par la Règle absolue ci-dessus (le script refuse ces pids en dur). **Protège toujours** la connexion courante (root) et l'infra (omega-mc, bots, daemons sécurité, tailscale, sshd). Tester d'abord avec `--dry`. +0. **Fermeture des tunnels/mosh détachés** (si demandé) : `scripts/close-sessions.sh` ferme les tunnels cloudflared + serveurs mosh détachés de `vibe`/`lab` — et RIEN d'autre : claude et rmux sont hors périmètre par la Règle absolue ci-dessus (le script refuse ces pids en dur). **Protège toujours** la connexion courante (root) et l'infra (Telegram bots, daemons sécurité, tailscale, sshd). Tester d'abord avec `--dry`. 1. **Garde-fou** : `scripts/idle-check.sh`. Si occupé → rapport + stop. 2. **Analyse** : `scripts/analyze.sh` → df, top du `/home` `/tmp` `/var`, `docker system df`. 3. **Purge auto-sûre** : `scripts/safe-clean.sh` → `docker builder prune`, `apt-get clean`, `journalctl --vacuum-size=100M`. (rien de Station, rien de doc.) diff --git a/skills/cleanup/scripts/close-sessions.sh b/skills/cleanup/scripts/close-sessions.sh index 9d7c9901..87f22dd2 100755 --- a/skills/cleanup/scripts/close-sessions.sh +++ b/skills/cleanup/scripts/close-sessions.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # Ferme les tunnels cloudflared + serveurs mosh détachés des users de dev (vibe, lab). -# PROTÈGE toujours : la connexion courante (root), l'infra (omega-mc, bots Telegram, +# PROTÈGE toujours : la connexion courante (root), l'infra (Telegram bots, # daemons sécurité agentik-*, tailscaled, sshd, systemd). Usage: close-sessions.sh [--dry] # # ═══ RÈGLE ABSOLUE (opérateur, 2026-08-11, après incident) ═════════════════════ @@ -39,4 +39,4 @@ for p in $(pgrep -x cloudflared 2>/dev/null); do act "$p"; done echo "== Serveurs mosh détachés (vibe + lab ; root/connexion courante protégés) ==" for p in $(pgrep -x mosh-server -U vibe 2>/dev/null; pgrep -x mosh-server -U lab 2>/dev/null); do act "$p"; done -echo "(claude, rmux, root, infra omega-mc/bots/sécurité/tailscale/sshd : JAMAIS touchés)" +echo "(claude, rmux, root, infra bots/sécurité/tailscale/sshd : JAMAIS touchés)" diff --git a/telegram-bot/omega-tg-bot.ts b/telegram-bot/omega-tg-bot.ts index 23af8b74..f00aab04 100644 --- a/telegram-bot/omega-tg-bot.ts +++ b/telegram-bot/omega-tg-bot.ts @@ -6,7 +6,7 @@ * keyboard of sub-actions; each button runs an `omega` CLI action on the host. * Group/forum mode: /setupgroup registers a supergroup (verifies the bot is * admin); /sync maps each project to a forum topic and routes topic messages to - * that project's oracle. /dashboard sends the Mission Control link. + * that project's oracle. * Single poller per bot token. config ← ~/.omega/telegram.toml. */ import { $ } from "bun"; @@ -35,7 +35,6 @@ import { randomUUID } from "node:crypto"; const OMEGA_DIR = process.env.OMEGA_DIR || `${homedir()}/.omega`; const TG_TOML = `${OMEGA_DIR}/telegram.toml`; -const MC_ENV = `${OMEGA_DIR}/repos/omega-mc/.env`; const GROUPS_FILE = `${OMEGA_DIR}/telegram-groups.json`; const OMEGA = process.env.OMEGA_BIN || `${homedir()}/.local/bin/omega`; const READY_FILE = process.env.OMEGA_TG_READY_FILE || ""; @@ -354,8 +353,7 @@ function saveReconciledJson( // ── Conversation history: persisted per chat+topic so Atlas, the project oracles, // and the agent-bots all have FULL access to the running conversation (not stateless -// per-message). Stored as JSONL in ~/.omega/state/tg-history/ and mirrored to the -// OmegaMC dashboard (mcMirror) so the dashboard stays in sync with Telegram. +// per-message). Stored as JSONL in ~/.omega/state/tg-history/. const HIST_DIR = `${OMEGA_DIR}/state/tg-history`; const histKey = (chat: number, thread?: number) => `${chat}${thread ? `-t${thread}` : ""}`; const histPath = (chat: number, thread?: number) => `${HIST_DIR}/${histKey(chat, thread)}.jsonl`; @@ -365,7 +363,6 @@ function histAppend(chat: number, thread: number | undefined, role: "operator" | const line = JSON.stringify({ ts: new Date().toISOString(), role, text: String(text).slice(0, 8000) }) + "\n"; const p = histPath(chat, thread); writeFileSync(p, (existsSync(p) ? readFileSync(p, "utf8") : "") + line); - mcMirror(project || "atlas", role, text).catch(() => {}); } catch {} } // Last N turns as a plain transcript, to prepend to a brain/dispatch prompt. @@ -376,35 +373,15 @@ function histContext(chat: number, thread?: number, n = 12): string { return turns.length ? `## Recent history of this conversation (for context)\n${turns.join("\n")}\n\n` : ""; } catch { return ""; } } -// Mirror a turn into the OmegaMC dashboard store (best-effort) so the dashboard's -// per-agent conversation stays in sync with Telegram. Auto-disables after a failure -// (e.g. the MC build has no message-ingest endpoint yet) so it never spams. -let MC_MIRROR_OK = true; -async function mcMirror(agent: string, role: string, text: string) { - if (!MC_MIRROR_OK) return; - try { - const pw = MC_PW; if (!pw) { MC_MIRROR_OK = false; return; } - // Atlas's dashboard agent is "director" (there is no "atlas" agent in MC). - const id = (agent.toLowerCase() === "atlas" ? "director" : agent.toLowerCase()).replace(/[^a-z0-9_-]/g, "-"); - const r = await fetch(`http://localhost:8080/api/agents/definitions/${id}/messages`, { - method: "POST", - headers: { "content-type": "application/json", authorization: "Basic " + Buffer.from(":" + pw).toString("base64") }, - body: JSON.stringify({ role: role === "operator" ? "user" : "assistant", content: String(text).slice(0, 8000), source: "telegram" }), - }); - if (!r.ok) MC_MIRROR_OK = false; // endpoint absent (GET-only build) → stop trying - } catch { MC_MIRROR_OK = false; } -} - function readKV(path: string, re: RegExp): Record { const out: Record = {}; try { for (const l of readFileSync(path, "utf8").split("\n")) { const m = l.match(re); if (m) out[m[1]] = m[2].replace(/^"|"$/g, ""); } } catch {} return out; } -// Voice input: an OpenAI key (provisioning/services.env or the MC .env) enables -// Whisper transcription so you can TALK to Atlas. Empty → voice messages are ignored. +// Voice input: an OpenAI key (provisioning/services.env) enables Whisper +// transcription so you can TALK to Atlas. Empty → voice messages are ignored. const OPENAI_KEY = - readKV(`${OMEGA_DIR}/provisioning/services.env`, /^\s*export\s+([A-Z_]+)\s*=\s*"?([^"]*)"?\s*$/).OPENAI_API_KEY || - readKV(MC_ENV, /^([A-Z_]+)=(.*)$/).OPENAI_API_KEY || ""; + readKV(`${OMEGA_DIR}/provisioning/services.env`, /^\s*export\s+([A-Z_]+)\s*=\s*"?([^"]*)"?\s*$/).OPENAI_API_KEY || ""; // Whisper AUTO-DETECTS the language — we never pass one, so a mixed FR/EN operator // is transcribed correctly either way. `verbose_json` (not the default `json`) is what // returns the detected `language`, at no extra cost, so we surface it in the echo: @@ -850,52 +827,11 @@ function mdToHtml(src: string): string { return s.replace(/\u0000(\d+)\u0000/g, (_m, i) => (codes[+i] !== undefined ? codes[+i] : _m)); } -// ── Project management: "add a project" = make it MANAGED (dashboard + oracle + topic) -const MC_CONFIG = `${OMEGA_DIR}/repos/omega-mc/config/omega-mc.yaml`; -// Add a project's dedicated oracle to the Mission-Control roster (idempotent) so it -// shows in the dashboard like the 14 managers + the atlas. omega-mc hot-reloads it. +// ── Project management: "add a project" = make it MANAGED (oracle + topic) const projId = (name: string) => name.toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/^-+|-+$/g, ""); -// Remove a project's oracle entry from the Mission-Control roster (idempotent). -function mcUnregister(name: string): boolean { - try { - const id = projId(name); if (!id) return false; - let y = readFileSync(MC_CONFIG, "utf8"); - // Strip the ` :` block up to (but not including) the next top-level-2-space key. - const re = new RegExp(`\\n ${id}:\\n(?: {4,}.*\\n|\\n)*`, "g"); - if (!re.test(y)) return false; - y = y.replace(re, "\n"); - writeFileSync(MC_CONFIG, y); - return true; - } catch { return false; } -} -function mcRegister(name: string): "added" | "exists" | "skip" { - try { - const id = projId(name); - if (!id) return "skip"; - let y = readFileSync(MC_CONFIG, "utf8"); - if (new RegExp(`\\n ${id}:\\s`).test(y)) return "exists"; - const entry = -` ${id}: - description: "Project oracle for ${name} — dedicated orchestrator (multi-session); Atlas dispatches this project's missions here." - model: "claude-opus-5" - image: "omega-mc-agent:latest" - workspace: ${id} - claude_md: "${id}/CLAUDE.md" - nix_enabled: true - allowed_tools: [Read, Write, Edit, Bash, Glob, Grep, WebSearch, WebFetch] - env: - EDITOR: vim -`; - if (!/\nagents:\n/.test(y)) return "skip"; - y = y.replace(/\nagents:\n/, `\nagents:\n${entry}\n`); - writeFileSync(MC_CONFIG, y); - return "added"; - } catch { return "skip"; } -} -// Register a project as managed: dashboard entry + a Telegram topic (if the hub is a +// Register a project as managed: a Telegram topic (if the hub is a // forum supergroup and the bot is admin) + confirm its dedicated oracle is dispatchable. async function addProject(name: string, dir?: string): Promise { - const dash = mcRegister(name); const pdir = dir || repoPath(name) || ""; recordProject(name, pdir, pdir.split("/Station/")[1]?.split("/")[0] || ""); const g = loadGroups(); @@ -905,19 +841,17 @@ async function addProject(name: string, dir?: string): Promise { if (r.ok) { g.topics ||= {}; g.topics[String(r.result.message_thread_id)] = name; saveGroups(g); recordProject(name, pdir, undefined, r.result.message_thread_id); topicLine = "✅ Telegram topic created in the group."; } else topicLine = `⚠️ Topic not created: ${esc(r.description || "error")}.${/rights/i.test(r.description || "") ? " Enable the “Manage Topics” permission for the bot (group admin)." : ""}`; } - const dashLine = dash === "added" ? "added ✅" : dash === "exists" ? "already present ✅" : "not written ⚠️ (omega-mc config not found)"; await refreshCommands().catch(() => {}); // publish its /{project} command return card("PROJECT MANAGED", ` 📁 ${esc(name)}\n\n` + `• Dedicated oracle (multi-session): omega dispatch ${esc(name)} ✅\n` + - `• Mission Control dashboard: ${dashLine}\n` + `• ${topicLine}`, `Talk about the project in its topic (or here) — Atlas knows the context and directs its oracle.`); } // Import an existing GitHub repo as a managed project: clone it into -// ~/Station//, then wire the full OmegaOS setup (dashboard agent + -// shared registry + Telegram topic + /{project} command) — same footprint as a New +// ~/Station//, then wire the full OmegaOS setup (shared +// registry + Telegram topic + /{project} command) — same footprint as a New // project, minus the scaffold (the code comes from GitHub). `repoArg` accepts a full // URL (https/ssh) or an `owner/repo` slug; cloning uses `gh` (operator auth) so // private repos work too. @@ -936,9 +870,8 @@ async function importFromGithub(category: string, repoArg: string): Promise${esc(name)}\nClone failed:\n
${esc((cl.stdout.toString() + cl.stderr.toString()).trim().slice(0, 400))}
\n\nGive a public URL, owner/repo, or ensure gh can access a private repo.`); } const steps: string[] = [`📁 Cloned ${esc(slug || arg)}${esc(dir)} ✅`]; - const dash = mcRegister(name); recordProject(name, dir, category); - steps.push(`🤖 Oracle agent (dashboard): ${dash === "added" ? "created ✅" : dash === "exists" ? "already there ✅" : "⚠️ (omega-mc config not found)"}`); + steps.push("🤖 Dedicated oracle: dispatchable ✅"); const g = loadGroups(); if (g.hub && g.isForum) { const r = await tg("createForumTopic", { chat_id: g.hub, name: name.slice(0, 128) }); @@ -1152,7 +1085,7 @@ async function removeProjectTopic(name: string): Promise<"deleted" | "none" | st return r.description || "failed"; } // Delete a managed project. Three CUMULATIVE scopes (escalating): -// "omega" — remove from OmegaOS view only: Telegram topic + dashboard roster + +// "omega" — remove from OmegaOS view only: Telegram topic + // agent-bot + registry. The code (local folder + GitHub) stays. // "local" — omega + kill the oracle session + delete the LOCAL FOLDER (rm -rf, // off the VPS disk). GitHub repo is kept (your code stays on GitHub). @@ -1171,9 +1104,7 @@ async function deleteProject(name: string, mode: "omega" | "local" | "all"): Pro // 1. Telegram topic const topic = await removeProjectTopic(name); steps.push(topic === "deleted" ? "💬 Telegram topic: deleted ✅" : topic === "none" ? "💬 Topic: (none)" : `💬 Topic: ⚠️ ${esc(topic)}`); - // 2. Dashboard roster - steps.push(mcUnregister(name) ? "🤖 Dashboard agent: removed ✅" : "🤖 Dashboard agent: (absent)"); - // 3. Agent-bot service (if one was associated) + // 2. Agent-bot service (if one was associated) const bots = loadAgentBots(); if (bots[id] || bots[name]) { updateAgentBots(latest => { delete latest[id]; delete latest[name]; }); @@ -1212,7 +1143,7 @@ async function deleteProject(name: string, mode: "omega" | "local" | "all"): Pro // /delete command. Three escalating tiers, in order (omega → local → all). function projDeleteMenu(name: string): { text: string; markup: any } { return { - text: card("DELETE PROJECT", ` 🗑 ${esc(name)} — choose how far to go:\n\n1️⃣ Remove from OmegaOS — Telegram topic, dashboard agent, agent-bot, registry. Local folder + GitHub stay.\n2️⃣ Delete local machine — that + deletes the local folder off the VPS (irreversible). GitHub kept.\n3️⃣ Delete all (+ GitHub) — that + deletes the GitHub repo (irreversible). Nothing remains.`), + text: card("DELETE PROJECT", ` 🗑 ${esc(name)} — choose how far to go:\n\n1️⃣ Remove from OmegaOS — Telegram topic, agent-bot, registry. Local folder + GitHub stay.\n2️⃣ Delete local machine — that + deletes the local folder off the VPS (irreversible). GitHub kept.\n3️⃣ Delete all (+ GitHub) — that + deletes the GitHub repo (irreversible). Nothing remains.`), markup: kb([ [{ text: "1️⃣ Remove from OmegaOS", callback_data: `proj:delomega:${name}`.slice(0, 64) }], [{ text: "2️⃣ Delete local machine", callback_data: `proj:dellocal:${name}`.slice(0, 64) }], @@ -1232,7 +1163,7 @@ function stationCategories(): string[] { return cats.length ? cats : ["Clients", "SideBusiness", "Lab", "LifeStyle"]; } -// New project end-to-end: folder + git + README, dashboard oracle agent, managed +// New project end-to-end: folder + git + README, managed // registry, and a Telegram topic (when the group is a forum + the bot is admin). async function createProject(category: string, name: string, desc: string): Promise<{ dir: string; report: string }> { const safe = name.replace(/[^A-Za-z0-9._-]/g, "-").replace(/^-+|-+$/g, "") || "project"; @@ -1240,9 +1171,8 @@ async function createProject(category: string, name: string, desc: string): Prom const steps: string[] = []; const mk = Bun.spawnSync(["bash", "-lc", `mkdir -p ${dir} && cd ${dir} && (git rev-parse --git-dir >/dev/null 2>&1 || git init -q) && printf '# %s\\n\\n%s\\n' ${JSON.stringify(safe)} ${JSON.stringify(desc)} > README.md && git add -A 2>/dev/null; echo ok`]); steps.push(mk.stdout.toString().includes("ok") ? `📁 Folder + git: ${dir}` : `📁 Folder: ⚠️ ${esc(mk.stderr.toString().slice(0, 120))}`); - const dash = mcRegister(safe); recordProject(safe, dir, category); - steps.push(`🤖 Oracle agent (dashboard): ${dash === "added" ? "created ✅" : dash === "exists" ? "already there ✅" : "⚠️"}`); + steps.push("🤖 Dedicated oracle: dispatchable ✅"); const g = loadGroups(); if (g.hub && g.isForum) { const r = await tg("createForumTopic", { chat_id: g.hub, name: safe.slice(0, 128) }); @@ -2891,14 +2821,6 @@ async function abortCodexLogin(chat: number, msgId: number, pid: string) { kb([[{ text: "🔄 Re-login", callback_data: "acct:codex" }], [back("account")]])); } -function dashboardURL(): { url: string; pw: string } { - const mc = readKV(MC_ENV, /^([A-Z_]+)=(.*)$/); - const host = mc.HOSTNAME?.trim(); - const ip = (process.env.OMEGA_PUBLIC_IP || "").trim(); - // Only return a button-able URL when we actually have a host/IP (never http://:8080). - const url = host ? `https://${host}` : (ip ? `http://${ip}:8080` : ""); - return { url, pw: mc.OMEGA_MC_WEB_PASSWORD || "" }; -} async function resolvePublicIP(): Promise { if (process.env.OMEGA_PUBLIC_IP) return; for (const u of ["https://ifconfig.me/ip", "https://icanhazip.com", "https://api.ipify.org"]) { @@ -2935,15 +2857,25 @@ async function auditIds(): Promise { return [...new Set(ids)]; } -// ── command menu (setMyCommands list) ──────────────────────────────────────── -// OmegaMC dashboard API (read agents). Web password from omega-mc .env. -const MC_PW = readKV(MC_ENV, /^([A-Z_]+)=(.*)$/).OMEGA_MC_WEB_PASSWORD || ""; -async function mcAgents(): Promise<{ id: string; description?: string }[]> { - try { - const r = await fetch("http://localhost:8080/api/agents/definitions", { headers: { authorization: "Basic " + Buffer.from(":" + MC_PW).toString("base64") } }); - const j = await r.json(); return Array.isArray(j) ? j : (j.agents || []); - } catch { return []; } -} +// Native AISB roster — same 15 Matrix roles as omega_core::aisb_agents. +const AISB_ROSTER: { id: string; description: string }[] = [ + { id: "oracle", description: "Router · Intent classifier · Pipeline coordinator" }, + { id: "morpheus", description: "Executor · Code writer · Workhorse" }, + { id: "seraph", description: "Auditor · Skeptical reviewer · 6-phase quality gate" }, + { id: "keymaker", description: "Planner · Mission decomposer · DAG builder" }, + { id: "smith", description: "Self-improver · Pattern extractor · Evolution" }, + { id: "niobe", description: "Researcher · Web & codebase investigator" }, + { id: "architect", description: "System designer · Design-doc author" }, + { id: "merovingian", description: "Cross-project knowledge broker" }, + { id: "neo", description: "Health monitor · Stall detector" }, + { id: "zion", description: "Metrics dashboard · Cost reporter" }, + { id: "link", description: "Telegram notifier · External comms" }, + { id: "construct", description: "UI component lookup · shadcn/Radix" }, + { id: "pythia", description: "Docs watcher · Weekly Anthropic releases" }, + { id: "council", description: "Multi-model deliberation · Convener · President-synthesizer" }, + { id: "trinity", description: "White-hat security operator · Offensive + defensive · Pentest / AI red-team" }, +]; +function aisbAgents(): { id: string; description?: string }[] { return AISB_ROSTER; } const MENU: [string, string][] = [ ["start", "Welcome + quick status"], @@ -2952,7 +2884,6 @@ const MENU: [string, string][] = [ ["commands", "Show available commands"], ["agents", "List the AISB agents (talk via the agents bot)"], ["council", "Convene @council — judge panel for a high-stakes/contested decision"], - ["dashboard", "Open the Mission Control dashboard (link)"], ["status", "Live system status"], ["sessions", "Active sessions — Status / Kill"], ["projects", "Projects — list / new / add"], @@ -2972,7 +2903,7 @@ const MENU: [string, string][] = [ ]; // Commands with a dedicated button view/handler. Anything NOT here is routed to // the AISB Master brain instead of falling back to the menu (intelligent commands). -const KNOWN = new Set([...MENU.map(([c]) => c), "setupgroup", "sync", "dispatch", "zernio"]); +const KNOWN = new Set([...MENU.map(([c]) => c), "setupgroup", "sync", "dispatch", "zernio", "dashboard"]); function menuKb() { // The NOVA OS row controls the operator-built omega-novaos.service — only // show it where that unit exists (on a fresh install it was an always-broken @@ -2980,12 +2911,11 @@ function menuKb() { const hasNovaOS = existsSync(`${homedir()}/.config/systemd/user/omega-novaos.service`); return kb([ [{ text: "📖 Guide — how it works", callback_data: "nav:guide" }], - [{ text: "🤖 Agents", callback_data: "nav:agents" }, { text: "🖥 Dashboard", callback_data: "nav:dashboard" }], - [{ text: "📊 Status", callback_data: "nav:status" }, { text: "🗂 Sessions", callback_data: "nav:sessions" }], - [{ text: "📁 Projects", callback_data: "nav:projects" }, { text: "🔍 Audits", callback_data: "nav:audits" }], - [{ text: "💳 Account", callback_data: "nav:account" }, { text: "🧠 Model", callback_data: "nav:model" }], - [{ text: "🧩 Skills", callback_data: "nav:skills" }, { text: "🚀 Dispatch", callback_data: "nav:dispatch" }], - [{ text: "🌀 Zernio — publish", callback_data: "nav:zernio" }], + [{ text: "🤖 Agents", callback_data: "nav:agents" }, { text: "🗂 Sessions", callback_data: "nav:sessions" }], + [{ text: "📊 Status", callback_data: "nav:status" }, { text: "🔍 Audits", callback_data: "nav:audits" }], + [{ text: "📁 Projects", callback_data: "nav:projects" }, { text: "💳 Account", callback_data: "nav:account" }], + [{ text: "🧠 Model", callback_data: "nav:model" }, { text: "🧩 Skills", callback_data: "nav:skills" }], + [{ text: "🚀 Dispatch", callback_data: "nav:dispatch" }, { text: "🌀 Zernio — publish", callback_data: "nav:zernio" }], [{ text: "🚀 Marketing", callback_data: "nav:marketing" }], [{ text: "👥 Group hub", callback_data: "nav:setupgroup" }, { text: "🧹 Clean", callback_data: "nav:clean" }], ...(hasNovaOS ? [[{ text: "🤖 NOVA OS (status / kill-switch)", callback_data: "nav:novaos" }]] : []), @@ -3034,8 +2964,7 @@ async function guideCard(): Promise { ` 🔍 Audits — Quality Arsenal: 23 forensic audits\n` + ` 💳 Account — Claude login (one shared credential) + usage\n` + ` 🧠 Model — pick the AI provider + model\n` + - ` 🤖 Agents — a dedicated bot per project oracle\n` + - ` 🖥 Dashboard — Mission Control (web)\n` + + ` 🤖 Agents — AISB roster + dedicated bots (Nova / Trinity / Alexandria)\n` + ` 🚀 Dispatch — fire a mission at an oracle\n` + ` 👥 Group hub — supergroup: 1 topic = 1 project`); } @@ -3083,8 +3012,7 @@ function statusCard(raw: string): string { // ── model picker: provider → model, all clickable. Canonical lists come from the // Rust SSOT (`omega config models [provider]`); a mirror of providers.rs::models_for // is the fallback for binaries predating that subcommand. Selecting writes -// providers.toml (omega sessions) and, for claude, the omega-mc dashboard fallback -// (defaults.model only — the per-agent opus/sonnet split is preserved). +// providers.toml (omega sessions). const PROVIDER_FALLBACK = [ "claude", "codex", "gemini", "antigravity", "glm", "openrouter", "pi", "hermes", "kimi", @@ -3104,9 +3032,6 @@ const PROVIDER_ICON: Record = { claude: "🟣", codex: "🟢", gemini: "🔵", antigravity: "🚀", glm: "🟡", openrouter: "🌐", pi: "π", hermes: "⚕", kimi: "🌙", }; -// Claude alias → full model id the omega-mc yaml uses (mirror of dispatch.rs + the -// dashboard's model convention). Anything not aliased is passed through verbatim. -const CLAUDE_FULL_ID: Record = { opus: "claude-opus-5", sonnet: "claude-sonnet-5", haiku: "claude-haiku-4-5" }; async function listProviders(): Promise { const out = await omega(["config", "models"]); const ps = out.split("\n").map(s => s.trim()).filter(s => /^[a-z]+$/.test(s)); @@ -3121,18 +3046,6 @@ async function currentModel(provider: string): Promise { const v = (await omega(["config", "get", `${provider}.model`])).trim().split("\n")[0] || ""; return /error|unknown|no output/i.test(v) ? "" : v; } -// Update the dashboard's defaults.model (the FIRST `model:` in the yaml = the -// defaults block, before any agent). Per-agent models are untouched. Returns the -// full id written, or "" on no-op. omega-mc hot-reloads the file within ~3s. -function mcSetDefaultModel(fullId: string): string { - try { - const y = readFileSync(MC_CONFIG, "utf8"); - const next = y.replace(/(\n\s*model:\s*)"[^"]*"/, `$1"${fullId}"`); - if (next === y) return ""; - writeFileSync(MC_CONFIG, next); - return fullId; - } catch { return ""; } -} // Render the model list for a provider with the current pick marked ✓. // Is a provider's API key set? (omega() returns "(no output…" for an empty value.) async function providerHasKey(provider: string): Promise { @@ -3291,33 +3204,16 @@ async function view(name: string): Promise<{ text: string; markup: any }> { case "menu": case "help": case "commands": return { text: menuText, markup: menuKb() }; case "start": case "guide": return { text: await guideCard(), markup: kb([[{ text: "📋 Open menu", callback_data: "nav:menu" }], [{ text: "🚀 Dispatch", callback_data: "nav:dispatch" }, { text: "💳 Account", callback_data: "nav:account" }]]) }; case "agents": { - // The companion (Nova) link lives HERE — it is the only flow that creates - // a kind:"companion" agent-bot entry, so it must not depend on the - // optional MC dashboard being up. const novaRow: Btn[] = [{ text: "💞 Link your companion (Nova)", callback_data: "agent:tglink:nova" }]; - // Like Nova, the security operator (Trinity) binds to its own bot from here — - // its own kind:"security" entry, independent of the optional MC dashboard. const trinityRow: Btn[] = [{ text: "🛡 Link your security agent (Trinity)", callback_data: "agent:tglink:trinity" }]; - // Like Nova/Trinity, the Librarian (Alexandria) binds its own bot from here — - // a kind:"persona" entry pointing at the shipped ALEXANDRIA OS system prompt. const libRow: Btn[] = [{ text: "📚 Link your librarian (Alexandria)", callback_data: "agent:tglink:librarian" }]; - const ags = await mcAgents(); - if (!ags.length) return { text: card("AISB AGENTS", " ⚠️ Dashboard unreachable. Start it: omega-mc-up.\n\n 💞 You can still link your personal companion bot (Nova), 🛡 security agent (Trinity) and 📚 librarian (Alexandria) below."), markup: kb([novaRow, trinityRow, libRow, [back()]]) }; + const ags = aisbAgents(); const rows: Btn[][] = []; for (let i = 0; i < ags.length; i += 2) rows.push(ags.slice(i, i + 2).map(a => ({ text: a.id.slice(0, 28), callback_data: `agent:info:${a.id}`.slice(0, 64) }))); - return { text: card(`AISB AGENTS — ${ags.length}`, " Tap an agent for its role. To talk to it, use its dedicated bot (see /dashboard).\n 💞 “Link your companion” wires Nova — your personal assistant on her own bot.\n 🛡 “Link your security agent” wires Trinity — a white-hat pentest operator on its own bot.\n 📚 “Link your librarian” wires Alexandria — turns any book or idea into understanding, memory and action."), markup: kb([...rows, novaRow, trinityRow, libRow, [back()]]) }; + return { text: card(`AISB AGENTS — ${ags.length}`, " Tap an agent for its role. Link a dedicated bot to talk to it directly.\n 💞 “Link your companion” wires Nova — your personal assistant on her own bot.\n 🛡 “Link your security agent” wires Trinity — a white-hat pentest operator on its own bot.\n 📚 “Link your librarian” wires Alexandria — turns any book or idea into understanding, memory and action."), markup: kb([...rows, novaRow, trinityRow, libRow, [back()]]) }; } case "dashboard": { - await resolvePublicIP(); - const { url } = dashboardURL(); - const rows: Btn[][] = []; - if (url) rows.push([{ text: "👉 Tap here to open", url }]); - rows.push([{ text: "🔑 Reveal the password", callback_data: "dash:pw" }]); - rows.push([back()]); - const body = url - ? ` ${esc(url)}\n\n Tap “👉 Open” for the dashboard, then “🔑 Reveal” for the password.` - : ` ⚠️ Public IP not resolved — try again, or enable Tailscale for secure access.`; - return { text: card("MISSION CONTROL", body), markup: kb(rows) }; + return { text: card("MISSION CONTROL", " Retired. Phone control is this Telegram bot — use /menu. No separate web dashboard."), markup: kb([[back()]]) }; } case "status": return { text: statusCard(await omega(["doctor"])), markup: kb([[{ text: "🛠 Fix it", callback_data: "status:fix" }, { text: "🔄 Refresh", callback_data: "nav:status" }], [back()]]) }; case "sessions": { @@ -3474,24 +3370,10 @@ async function onCallback(data: string, chat: number, msgId: number, from: numbe const i = arg.indexOf(":"); const provider = arg.slice(0, i); const model = arg.slice(i + 1); const res = await omega(["config", "activate", provider, model]); const okOmega = /^\[\+\] Active provider/m.test(res); - let dash = ""; - if (provider === "claude") { - const full = CLAUDE_FULL_ID[model] || model; - const wrote = mcSetDefaultModel(full); - dash = `\n 🖥 Dashboard defaults: ${wrote ? `${esc(full)}(hot-reload ~3s)` : "unchanged"}`; - } - const banner = ` ${okOmega ? "✅" : "⚠️"} ${esc(provider)}${esc(model)}\n ⚙️ global default for new sessions: ${okOmega ? "✅" : "⚠️ " + esc(res.slice(0, 80))}${dash}`; + const banner = ` ${okOmega ? "✅" : "⚠️"} ${esc(provider)}${esc(model)}\n ⚙️ global default for new sessions: ${okOmega ? "✅" : "⚠️ " + esc(res.slice(0, 80))}`; const v = await modelProviderView(provider, banner); return edit(chat, msgId, v.text, v.markup); } - if (ns === "dash" && action === "pw") { - const { pw } = dashboardURL(); - if (!pw) return; - // Reveal in a copyable code block, then auto-delete after 30s (so it never lingers in chat history). - const m = await tg("sendMessage", { chat_id: chat, parse_mode: "HTML", text: `🔑 Dashboard password\n(tap it to copy — disappears in 30s)\n\n${esc(pw)}` }); - if (m.ok) setTimeout(() => tg("deleteMessage", { chat_id: chat, message_id: m.result.message_id }), 30000); - return; - } if (ns === "sess" && action === "status") return edit(chat, msgId, pre(`Session ${arg}`, await omega(["capture", arg])), kb([[{ text: "🔄 Refresh", callback_data: `sess:status:${arg}`.slice(0, 64) }, back("sessions")]])); if (ns === "sess" && action === "kill") return edit(chat, msgId, pre(`Kill ${arg}`, await omega(["kill", arg])), kb([[back("sessions")]])); if (ns === "proj" && action === "list") { const v = await view("projects"); return edit(chat, msgId, v.text, v.markup); } @@ -3688,7 +3570,7 @@ async function onCallback(data: string, chat: number, msgId: number, from: numbe for (let i = 0; i < 12; i++) { await Bun.sleep(600); try { const t = readFileSync(outf, "utf8"); if (t.trim().length > 40) { out = t; break; } } catch {} } return edit(chat, msgId, pre("💾 Purge RAM — terminé", out || "⏳ déclenché — résultat indisponible (helper agentik-ramflush actif ?)"), kb([[back("clean")]])); } - if (ns === "agent" && action === "info") { const a = (await mcAgents()).find(x => x.id === arg); return edit(chat, msgId, `🤖 ${esc(arg)}\n${esc(a?.description || "(no description)")}\n\nLink a dedicated Telegram bot to this agent — you'll talk to it directly (scoped to its project).`, kb([[{ text: "🔗 Link Telegram", callback_data: `agent:tglink:${arg}`.slice(0, 64) }], [back("agents")]])); } + if (ns === "agent" && action === "info") { const a = aisbAgents().find(x => x.id === arg); return edit(chat, msgId, `🤖 ${esc(arg)}\n${esc(a?.description || "(no description)")}\n\nLink a dedicated Telegram bot to this agent — you'll talk to it directly (scoped to its project).`, kb([[{ text: "🔗 Link Telegram", callback_data: `agent:tglink:${arg}`.slice(0, 64) }], [back("agents")]])); } if (ns === "agent" && action === "tglink") { setPending(from, "tg-link", arg); const body = /^(nova|companion)$/i.test(arg)