Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
6 changes: 3 additions & 3 deletions agents/aisb-atlas.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion agents/aisb/atlas.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
31 changes: 3 additions & 28 deletions crates/omega-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8425,7 +8425,7 @@ async fn cmd_spawn_worker(
dir: Option<&str>,
project: Option<&str>,
files: Option<Vec<String>>,
force: bool,
_force: bool,
worktree: bool,
agent_override: Option<&str>,
) -> Result<()> {
Expand All @@ -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()),
Expand Down Expand Up @@ -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) => {
Expand Down
85 changes: 60 additions & 25 deletions crates/omega-core/src/agents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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-
Expand Down Expand Up @@ -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,
Expand All @@ -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
)),
}
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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 [
Expand Down
62 changes: 21 additions & 41 deletions crates/omega-core/src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
);
Expand Down Expand Up @@ -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")
})?;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
}
Loading
Loading