Skip to content
Open
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
9 changes: 8 additions & 1 deletion apps/ios/Zeron/Models/HarnessCatalog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ struct ModelInfo: Identifiable, Hashable {

enum HarnessCatalog {
/// Static fallback = the engine's `default_enabled()` pair. The ACP
/// agents (grok/hermes/pi) appear only through a device's live
/// agents (grok/hermes/pi/oh-my-pi) appear only through a device's live
/// `ListHarnesses` catalog — they're opt-in per device via
/// Settings → Agents on the desktop.
static let harnesses: [HarnessInfo] = [
Expand All @@ -37,6 +37,7 @@ enum HarnessCatalog {
"grok": "Grok",
"hermes": "Hermes",
"pi": "Pi",
"oh-my-pi": "Oh My Pi",
"cursor": "Cursor",
"mock": "Mock",
]
Expand Down Expand Up @@ -72,6 +73,12 @@ enum HarnessCatalog {
description: "Runs the model configured in pi (`pi` settings)",
reasoningLevels: ["minimal", "low", "medium", "high", "xhigh", "max"]),
]
case "oh-my-pi":
return [
ModelInfo(id: "default", label: "omp default",
description: "Runs the model configured in omp settings",
reasoningLevels: ["minimal", "low", "medium", "high", "xhigh", "max"]),
]
case "codex":
return [
ModelInfo(id: "gpt-5.6-sol", label: "GPT-5.6-Sol",
Expand Down
2 changes: 1 addition & 1 deletion apps/ios/Zeron/Theme/BrandMarks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ enum BrandMark {
case "cursor": return .cursor
case "grok": return .grok
case "hermes": return .hermes
case "pi": return .pi
case "pi", "oh-my-pi": return .pi
default: return .claude // claude-code + mock share the mark, like the desktop
}
}
Expand Down
1 change: 1 addition & 0 deletions apps/zeron/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ fn harness_from_env() -> zeron_engine::HarnessId {
Ok("grok") => zeron_engine::HarnessId::Grok,
Ok("hermes") => zeron_engine::HarnessId::Hermes,
Ok("pi") => zeron_engine::HarnessId::Pi,
Ok("oh-my-pi") => zeron_engine::HarnessId::OhMyPi,
_ => zeron_engine::HarnessId::ClaudeCode,
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/engine/src/agent_accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1298,6 +1298,7 @@ fn harness_slug(harness: HarnessId) -> &'static str {
HarnessId::Grok => "grok",
HarnessId::Hermes => "hermes",
HarnessId::Pi => "pi",
HarnessId::OhMyPi => "oh-my-pi",
HarnessId::Mock => "mock",
}
}
Expand Down
42 changes: 40 additions & 2 deletions crates/engine/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,28 @@ pub fn default_registry() -> HarnessRegistry {
Box::new(|| zeron_harness::AcpHarness::pi().installed()),
Box::new(|| Ok(Arc::new(zeron_harness::AcpHarness::pi()) as Arc<dyn Harness>)),
);
// Oh My Pi over ACP (`omp acp`). Separate product from Pi — do not
// share a slot. Same lazy pattern as Hermes.
registry.register_lazy(
HarnessDescriptor {
id: HarnessId::OhMyPi,
name: "Oh My Pi".into(),
supports_steering: true,
steering_mode: SteeringMode::TurnBoundary,
reasoning_levels: vec![
ReasoningLevel::Minimal,
ReasoningLevel::Low,
ReasoningLevel::Medium,
ReasoningLevel::High,
ReasoningLevel::XHigh,
ReasoningLevel::Max,
],
installed: true,
enabled: None,
},
Box::new(|| zeron_harness::AcpHarness::oh_my_pi().installed()),
Box::new(|| Ok(Arc::new(zeron_harness::AcpHarness::oh_my_pi()) as Arc<dyn Harness>)),
);
registry
}

Expand Down Expand Up @@ -510,7 +532,8 @@ mod tests {
HarnessId::Cursor,
HarnessId::Grok,
HarnessId::Hermes,
HarnessId::Pi
HarnessId::Pi,
HarnessId::OhMyPi
]
);
assert!(registry.resolve(HarnessId::Mock).is_ok());
Expand All @@ -533,7 +556,7 @@ mod tests {
ReasoningLevel::High
]
);
// Cursor, Hermes and Pi mirror their specs the same way.
// Cursor, Hermes, Pi and Oh My Pi mirror their specs the same way.
let cursor = registry.resolve(HarnessId::Cursor).unwrap();
assert_eq!(cursor.id(), HarnessId::Cursor);
assert_eq!(cursor.display_name(), "Cursor");
Expand All @@ -559,6 +582,21 @@ mod tests {
ReasoningLevel::Max
]
);
let omp = registry.resolve(HarnessId::OhMyPi).unwrap();
assert_eq!(omp.id(), HarnessId::OhMyPi);
assert_eq!(omp.display_name(), "Oh My Pi");
assert_eq!(omp.steering_mode(), SteeringMode::TurnBoundary);
assert_eq!(
omp.reasoning_levels(),
&[
ReasoningLevel::Minimal,
ReasoningLevel::Low,
ReasoningLevel::Medium,
ReasoningLevel::High,
ReasoningLevel::XHigh,
ReasoningLevel::Max
]
);
}

/// Catalogs serialized by engines that predate the `installed`/`enabled`
Expand Down
84 changes: 83 additions & 1 deletion crates/harness/src/acp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
//! implementation covers every ACP agent; [`AcpHarness::grok`] configures it
//! for xAI's Grok Build (`grok agent stdio`), the first registered agent —
//! [`AcpHarness::hermes`] (Nous Research, `hermes acp`), [`AcpHarness::pi`]
//! (pi.dev via `pi-acp`) and [`AcpHarness::cursor`] (`cursor-agent acp`)
//! (pi.dev via `pi-acp`), [`AcpHarness::oh_my_pi`] (`omp acp`)
//! and [`AcpHarness::cursor`] (`cursor-agent acp`)
//! followed.
//!
//! - `initialize` (protocolVersion 1, fs/terminal capabilities declined) →
Expand Down Expand Up @@ -130,6 +131,10 @@ pub(crate) fn find_on_paths(exe: &str, extra: Vec<PathBuf>) -> Option<PathBuf> {
candidates.into_iter().find(|p| p.exists())
}

fn oh_my_pi_install_paths() -> Vec<PathBuf> {
npm_global_bins("omp")
}

/// Generic effort ladder for agents without their own clamping rules.
fn default_effort_values(
reasoning: Option<ReasoningLevel>,
Expand Down Expand Up @@ -483,6 +488,57 @@ fn pi_spec() -> AcpAgentSpec {
}
}

fn oh_my_pi_spec() -> AcpAgentSpec {
AcpAgentSpec {
id: HarnessId::OhMyPi,
display_name: "Oh My Pi",
executable: "omp",
env_override: "OMP_EXECUTABLE",
args: &["acp"],
npm_package: None,
extra_paths: oh_my_pi_install_paths,
cli_executable: "omp",
cli_extra_paths: oh_my_pi_install_paths,
install_hint: "omp (searched PATH, the login shell's PATH, npm global bins, \
and fnm/nvm/volta/pnpm/bun install dirs; install with \
`curl -fsSL https://omp.sh/install | sh`; set OMP_EXECUTABLE to override)",
// Models come from the agent's own provider config (`~/.omp`);
// the picker advertises a pass-through entry and the agent keeps whatever
// the user set up. Unknown ids are skipped by the config-option set.
models: || {
vec![Model {
id: "default".into(),
label: "omp default".into(),
description: Some("Runs the model configured in omp settings".into()),
reasoning_levels: vec![
ReasoningLevel::Minimal,
ReasoningLevel::Low,
ReasoningLevel::Medium,
ReasoningLevel::High,
ReasoningLevel::XHigh,
ReasoningLevel::Max,
],
options: Vec::new(),
}]
},
// Native `omp acp` does not advertise `_session/steering`:
// turn boundaries. Thinking ladder is minimal→max ("off" has no zeron
// tier and is left to the agent default).
steering_mode: SteeringMode::TurnBoundary,
reasoning_levels: &[
ReasoningLevel::Minimal,
ReasoningLevel::Low,
ReasoningLevel::Medium,
ReasoningLevel::High,
ReasoningLevel::XHigh,
ReasoningLevel::Max,
],
prompt_transform: identity_transform,
effort_values: default_effort_values,
ladder_extras: &[],
}
}

/// Background-install managed npm adapters for agents whose CLI is present
/// on this device, so a first chat never pays (or trips over) an npm run.
/// Skips agents whose adapter is already resolvable; failures are logged and
Expand Down Expand Up @@ -600,6 +656,11 @@ impl AcpHarness {
Self::with_spec(pi_spec())
}

/// Oh My Pi over ACP — native `omp acp`. A different product from Pi.
pub fn oh_my_pi() -> Self {
Self::with_spec(oh_my_pi_spec())
}

/// Use a fixed agent binary instead of PATH/known-location resolution.
pub fn with_executable(mut self, path: impl Into<PathBuf>) -> Self {
self.executable = Some(path.into());
Expand All @@ -619,6 +680,27 @@ impl AcpHarness {
self
}

/// Test seam: program + argv `run` would spawn.
#[doc(hidden)]
pub fn launch_command(&self) -> Result<(PathBuf, Vec<String>), HarnessError> {
match self.resolve_launch()? {
Launch::Program(program, args) => Ok((program, args)),
Launch::Managed {
pin,
bin_name,
args,
} => {
let program = match crate::adapter_install::installed_entry(&pin, bin_name) {
Some(entry) => entry,
None => crate::adapter_install::find_npm().ok_or_else(|| {
HarnessError::NotInstalled(self.spec.install_hint.into())
})?,
};
Ok((program, args))
}
}
}

/// Test seam: the program `run` would spawn (the adapter binary, or —
/// for a managed npm adapter — its installed entry, else npm as the
/// installer that would run first).
Expand Down
2 changes: 1 addition & 1 deletion crates/harness/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//! Every production harness is the shared [`AcpHarness`] with a per-agent
//! spec: Claude Code via the org-maintained `claude-agent-acp` adapter, Codex
//! via `codex-acp`, Cursor, Grok Build and Hermes natively, pi via the
//! community `pi-acp` adapter. Decision record:
//! community `pi-acp` adapter, Oh My Pi via native `omp acp`. Decision record:
//! docs/research/acp.md (the bespoke stream-json/app-server adapters this
//! crate used to hold are documented historically in
//! docs/research/harness.md).
Expand Down
23 changes: 23 additions & 0 deletions crates/harness/tests/acp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,10 @@ async fn claude_and_codex_specs_drive_the_same_wire() {
AcpHarness::hermes().with_executable(fixture_path()),
),
("pi", AcpHarness::pi().with_executable(fixture_path())),
(
"oh-my-pi",
AcpHarness::oh_my_pi().with_executable(fixture_path()),
),
(
"cursor",
AcpHarness::cursor().with_executable(fixture_path()),
Expand Down Expand Up @@ -981,6 +985,23 @@ fn hermes_and_pi_descriptor_surfaces_match_registry_expectations() {
zeron_proto::ReasoningLevel::Max,
]
);

let omp = AcpHarness::oh_my_pi();
assert_eq!(omp.id(), HarnessId::OhMyPi);
assert_eq!(omp.display_name(), "Oh My Pi");
assert!(omp.supports_steering());
assert_eq!(omp.steering_mode(), SteeringMode::TurnBoundary);
assert_eq!(
omp.reasoning_levels(),
&[
zeron_proto::ReasoningLevel::Minimal,
zeron_proto::ReasoningLevel::Low,
zeron_proto::ReasoningLevel::Medium,
zeron_proto::ReasoningLevel::High,
zeron_proto::ReasoningLevel::XHigh,
zeron_proto::ReasoningLevel::Max,
]
);
}

/// The 2026-08-12 stuck-Working wedge, end to end: a prompt whose turn was
Expand Down Expand Up @@ -1327,6 +1348,7 @@ async fn claude_busy_steer_rides_native_queueing_and_the_cost_frame() {
);
}


/// Every installed real agent, through the one shared loop all the
/// starve/settle changes live in: a short live turn with a mid-turn steer —
/// injection on StepBoundary agents, boundary delivery on TurnBoundary ones,
Expand All @@ -1343,6 +1365,7 @@ async fn real_all_harnesses_settle_with_a_mid_turn_steer() {
("cursor", AcpHarness::cursor()),
("grok", AcpHarness::grok()),
("pi", AcpHarness::pi()),
("oh-my-pi", AcpHarness::oh_my_pi()),
];
let mut failures: Vec<String> = Vec::new();
for (name, h) in agents {
Expand Down
1 change: 1 addition & 0 deletions crates/harness/tests/real_quiet_survey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ async fn real_all_harnesses_quiet_survey() {
("grok", AcpHarness::grok),
("hermes", AcpHarness::hermes),
("pi", AcpHarness::pi),
("oh-my-pi", AcpHarness::oh_my_pi),
];
let mut failures: Vec<String> = Vec::new();
for (name, ctor) in agents {
Expand Down
7 changes: 7 additions & 0 deletions crates/harness/tests/shell_env_resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ async fn cli_on_login_shell_path_only_is_resolved() {
write_executable(&shell_bin.join("codex-acp"), "#!/bin/sh\nexit 0\n");
write_executable(&shell_bin.join("claude-agent-acp"), "#!/bin/sh\nexit 0\n");
write_executable(&shell_bin.join("hermes"), "#!/bin/sh\nexit 0\n");
write_executable(&shell_bin.join("omp"), "#!/bin/sh\nexit 0\n");
write_executable(&shell_bin.join("pi-acp"), "#!/bin/sh\nexit 0\n");

// A $SHELL whose init shapes PATH — the shape resolution must survive.
Expand Down Expand Up @@ -51,6 +52,7 @@ async fn cli_on_login_shell_path_only_is_resolved() {
std::env::remove_var("CLAUDE_ACP_EXECUTABLE");
std::env::remove_var("HERMES_EXECUTABLE");
std::env::remove_var("PI_ACP_EXECUTABLE");
std::env::remove_var("OMP_EXECUTABLE");
std::env::remove_var("ZERON_NO_LOGIN_SHELL");
}

Expand All @@ -76,6 +78,11 @@ async fn cli_on_login_shell_path_only_is_resolved() {
.launch_program()
.expect("hermes resolves via login-shell PATH");
assert_eq!(hermes, shell_bin.join("hermes"), "{hermes:?}");
let (omp_prog, omp_args) = AcpHarness::oh_my_pi()
.launch_command()
.expect("omp acp resolves via login-shell PATH");
assert_eq!(omp_prog, shell_bin.join("omp"), "{omp_prog:?}");
assert_eq!(omp_args, vec!["acp".to_string()], "{omp_args:?}");
let pi = AcpHarness::pi()
.launch_program()
.expect("pi-acp resolves via login-shell PATH");
Expand Down
2 changes: 2 additions & 0 deletions crates/proto/src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ pub enum HarnessId {
Hermes,
/// The pi coding agent (pi.dev), driven over ACP via the `pi-acp` adapter.
Pi,
/// Oh My Pi (`omp`), a separate product, driven over ACP (`omp acp`).
OhMyPi,
/// Test harness; never shown in production pickers.
Mock,
}
Expand Down
2 changes: 1 addition & 1 deletion crates/ui/src/pickers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3292,7 +3292,7 @@ pub(crate) fn harness_brand_icon(harness: HarnessId) -> (&'static str, Option<gp
HarnessId::Grok => (crate::icons::GROK_MARK, None),
// Nous Research's mark (the Hermes product icon), monochrome.
HarnessId::Hermes => (crate::icons::HERMES_MARK, None),
HarnessId::Pi => (crate::icons::PI_MARK, None),
HarnessId::Pi | HarnessId::OhMyPi => (crate::icons::PI_MARK, None),
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/ui/src/settings/accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1202,7 +1202,7 @@ impl Render for AccountsPage {
HarnessId::Cursor => (crate::icons::CURSOR_MARK, None),
HarnessId::Grok => (crate::icons::GROK_MARK, None),
HarnessId::Hermes => (crate::icons::HERMES_MARK, None),
HarnessId::Pi => (crate::icons::PI_MARK, None),
HarnessId::Pi | HarnessId::OhMyPi => (crate::icons::PI_MARK, None),
_ => (
crate::icons::CLAUDE_MARK,
Some(crate::icons::claude_brand()),
Expand Down
2 changes: 2 additions & 0 deletions crates/ui/src/settings/harnesses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ pub fn blurb(harness: HarnessId) -> &'static str {
HarnessId::Grok => "xAI's Grok Build agent (grok CLI).",
HarnessId::Hermes => "Nous Research's Hermes Agent (hermes CLI).",
HarnessId::Pi => "The pi coding agent (pi CLI).",
HarnessId::OhMyPi => "Oh My Pi, driven through the omp CLI.",
HarnessId::Mock => "Scripted test harness.",
}
}
Expand All @@ -52,6 +53,7 @@ pub fn cli_name(harness: HarnessId) -> &'static str {
HarnessId::Grok => "grok",
HarnessId::Hermes => "hermes",
HarnessId::Pi => "pi",
HarnessId::OhMyPi => "omp",
HarnessId::Mock => "mock",
}
}
Expand Down
2 changes: 2 additions & 0 deletions docs/research/acp.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
ride pi's own provider config (catalog advertises a `default` pass-through
entry); thinking ladder minimal→max maps onto zeron's levels via the
generic `thought_level` preference ladder ("off" has no zeron tier).
`AcpHarness::oh_my_pi()` is a separate product: native `omp acp`
(`OMP_EXECUTABLE` overrides) when the `omp` CLI is on PATH.
- **ACP is the source of truth for model lists** (2026-08-08; preference
order inverted 2026-08-09): `models()` runs a short-lived probe
(initialize → `session/new`, the `discover_commands` pattern) and reads
Expand Down