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
10 changes: 7 additions & 3 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# comet-native — Architecture

A ground-up native rewrite of [comet](../comet) — a multi-device controller for coding agents
(Claude Code / Codex) — in Rust, with a gpui UI. Fresh app; no backwards compatibility required.
(Claude Code / Codex / Hermes) — in Rust, with a gpui UI. Fresh app; no backwards compatibility
required.

**Pillars (from the goal):**
- Sync is Loro CRDT docs (loro-mirror model) through Cloudflare Durable Objects.
Expand Down Expand Up @@ -138,7 +139,8 @@ comet-native/
# ephemeral presence, DocsStore (SQLite snapshots +
# processed-command ledger)
harness/ comet-harness # Harness trait + claude-code (stream-json subprocess),
# codex (app-server JSON-RPC), mock; steering mailbox,
# codex (app-server JSON-RPC), hermes (ACP over stdio),
# mock; shared jsonrpc client, steering mailbox,
# requestInput, models/reasoning/options catalogs
engine/ comet-engine # sessions engine (pub/sub, run journal, recovery, stall
# watchdog), doc host + command executor, repos/worktrees,
Expand Down Expand Up @@ -225,7 +227,9 @@ Direct ports of comet behaviors (spec: feature-inventory §3):
- **Harness** (research pending — `docs/research/harness.md`): trait mirroring comet's
`HarnessShape`; Claude Code via `claude` CLI stream-json in/out (control protocol for
permissions/AskUserQuestion→requestInput, resume, steering); Codex via app-server JSON-RPC or
`codex exec --json`; model/reasoning/option catalogs ported from `packages/harness`.
`codex exec --json`; Hermes via `hermes acp` (Agent Client Protocol — session/new+load,
session/prompt, session/update stream, session/request_permission, live model catalog);
model/reasoning/option catalogs ported from `packages/harness`.
- **Repos/diffs**: git2 or `git` subprocess (subprocess — matches comet, avoids libgit2 edge
cases); worktrees under `~/.comet-native/worktrees`; fs watchers (`notify`) + 2min repair; diff
capture (patch + numstat + untracked, 3MiB cap, sha256) → workspace doc summary + DO diff
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Comet

Control your coding agents (Claude Code, Codex) from any of your devices.
Control your coding agents (Claude Code, Codex, Hermes) from any of your devices.

![Comet running a Claude Code session](docs/screenshot.png)

Expand Down
1 change: 1 addition & 0 deletions apps/comet/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ fn harness_from_env() -> comet_engine::HarnessId {
Ok("mock") => comet_engine::HarnessId::Mock,
Ok("codex") => comet_engine::HarnessId::Codex,
Ok("cursor") => comet_engine::HarnessId::Cursor,
Ok("hermes") => comet_engine::HarnessId::Hermes,
_ => comet_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 @@ -1295,6 +1295,7 @@ fn harness_slug(harness: HarnessId) -> &'static str {
HarnessId::ClaudeCode => "claude-code",
HarnessId::Codex => "codex",
HarnessId::Cursor => "cursor",
HarnessId::Hermes => "hermes",
HarnessId::Mock => "mock",
}
}
Expand Down
61 changes: 42 additions & 19 deletions crates/engine/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,21 @@ pub fn default_registry() -> HarnessRegistry {
},
Box::new(|| Ok(Arc::new(comet_harness::CodexHarness::new()) as Arc<dyn Harness>)),
);
// Hermes (NousResearch/hermes-agent) over ACP, same lazy pattern. The
// static descriptor mirrors HermesHarness exactly: "Hermes", StepBoundary
// steering (a mid-turn prompt is redirected into the running turn), and an
// empty HARNESS-WIDE reasoning ladder. Hermes's live Codex/Claude model
// rows receive those underlying harnesses' model-specific traits instead.
registry.register_lazy(
HarnessDescriptor {
id: HarnessId::Hermes,
name: "Hermes".into(),
supports_steering: true,
steering_mode: SteeringMode::StepBoundary,
reasoning_levels: vec![],
},
Box::new(|| Ok(Arc::new(comet_harness::HermesHarness::new()) as Arc<dyn Harness>)),
);
registry
}

Expand Down Expand Up @@ -249,19 +264,26 @@ mod tests {
}

#[test]
fn default_registry_lists_mock_claude_and_codex_slots() {
fn default_registry_lists_mock_claude_codex_and_hermes_slots() {
let registry = default_registry();
let ids: Vec<HarnessId> = registry.descriptors().iter().map(|d| d.id).collect();
assert_eq!(
ids,
vec![HarnessId::Mock, HarnessId::ClaudeCode, HarnessId::Codex]
vec![
HarnessId::Mock,
HarnessId::ClaudeCode,
HarnessId::Codex,
HarnessId::Hermes
]
);
assert!(registry.resolve(HarnessId::Mock).is_ok());
assert!(registry.resolve(HarnessId::ClaudeCode).is_ok());
// A codex-configured chat resolves the right harness (construction is
// cheap; CLI discovery is deferred to models()/run()).
let codex = registry.resolve(HarnessId::Codex).unwrap();
assert_eq!(codex.id(), HarnessId::Codex);
let hermes = registry.resolve(HarnessId::Hermes).unwrap();
assert_eq!(hermes.id(), HarnessId::Hermes);
}

/// The Codex lazy descriptor must be indistinguishable from `describe()`
Expand All @@ -271,22 +293,23 @@ mod tests {
/// `[Ultrathink]` while the resolved adapter reports `[Low..Max]` — left
/// as-is here; flagged for its own pass.)
#[test]
fn codex_lazy_descriptor_matches_resolved_harness() {
let registry = default_registry();
let before = registry
.descriptors()
.into_iter()
.find(|d| d.id == HarnessId::Codex)
.unwrap();
registry.resolve(HarnessId::Codex).unwrap();
let after = registry
.descriptors()
.into_iter()
.find(|d| d.id == HarnessId::Codex)
.unwrap();
assert_eq!(before.name, after.name);
assert_eq!(before.supports_steering, after.supports_steering);
assert_eq!(before.steering_mode, after.steering_mode);
assert_eq!(before.reasoning_levels, after.reasoning_levels);
fn codex_and_hermes_lazy_descriptors_match_resolved_harnesses() {
for id in [HarnessId::Codex, HarnessId::Hermes] {
let registry = default_registry();
let find = |registry: &HarnessRegistry| {
registry
.descriptors()
.into_iter()
.find(|d| d.id == id)
.unwrap()
};
let before = find(&registry);
registry.resolve(id).unwrap();
let after = find(&registry);
assert_eq!(before.name, after.name, "{id:?}");
assert_eq!(before.supports_steering, after.supports_steering, "{id:?}");
assert_eq!(before.steering_mode, after.steering_mode, "{id:?}");
assert_eq!(before.reasoning_levels, after.reasoning_levels, "{id:?}");
}
}
}
7 changes: 4 additions & 3 deletions crates/harness/src/claude/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
//! - Interrupt: cancelling [`RunControls::interrupt`] sends the protocol-level
//! interrupt control request, then escalates to SIGTERM and SIGKILL.

mod catalog;
pub(crate) mod catalog;
mod normalize;
mod wire;

Expand Down Expand Up @@ -329,12 +329,13 @@ enum StdinMsg {

/// Anthropic's API caps inline images at 5MB of raw bytes; larger files stay
/// path refs only.
const MAX_INLINE_IMAGE_BYTES: u64 = 5 * 1024 * 1024;
pub(crate) const MAX_INLINE_IMAGE_BYTES: u64 = 5 * 1024 * 1024;

/// Media type for an inline image block — extension first, magic bytes as the
/// fallback (pasted screenshots may carry odd names). Only the API-supported
/// inline types map; anything else (svg/bmp/tiff/…) returns `None`.
fn image_media_type(path: &std::path::Path, bytes: &[u8]) -> Option<&'static str> {
/// Shared with the Hermes ACP adapter, which inlines the same staged uploads.
pub(crate) fn image_media_type(path: &std::path::Path, bytes: &[u8]) -> Option<&'static str> {
let by_ext = match path
.extension()
.and_then(|e| e.to_str())
Expand Down
5 changes: 2 additions & 3 deletions crates/harness/src/codex/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,8 @@
//! escalating to SIGTERM → SIGKILL if the child is unresponsive; the stream
//! always ends with `Done { status: Interrupted }`.

mod catalog;
pub(crate) mod catalog;
mod normalize;
mod rpc;

use std::collections::{HashSet, VecDeque};
use std::path::PathBuf;
Expand All @@ -47,12 +46,12 @@ use comet_proto::{
UserInputAnswer, UserInputQuestion,
};

use crate::jsonrpc::{Incoming, RpcClient};
use crate::{Harness, HarnessError, RunControls};
use catalog::{REASONING_LEVELS, sandbox_mode, sandbox_policy_value, static_models, to_effort};
use normalize::{
Phase, delta_text, item_id, item_type, map_item, turn_error_message, turn_id, usage_event,
};
use rpc::{Incoming, RpcClient};

/// Locate the device's installed Codex CLI: `CODEX_EXECUTABLE`, then our own
/// PATH, then the login-shell PATH snapshot (the user's shell init shapes
Expand Down
Loading