From c0bbab4e4e6ae2d8a16f0c79c3f4ed4a6dec3faa Mon Sep 17 00:00:00 2001 From: fajar Date: Sun, 30 Aug 2026 13:00:09 +0700 Subject: [PATCH 1/4] docs: feasibility analysis for a Cline harness integration --- docs/research/cline-harness.md | 146 +++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 docs/research/cline-harness.md diff --git a/docs/research/cline-harness.md b/docs/research/cline-harness.md new file mode 100644 index 000000000..d139a75cf --- /dev/null +++ b/docs/research/cline-harness.md @@ -0,0 +1,146 @@ +# Cline harness integration: feasibility analysis (2026-08) + +## Verdict + +**Feasible, low-to-medium effort (~1–2 days), via the shared `AcpHarness`.** The Cline CLI +ships a first-class ACP server (`cline --acp`) built ground-up on the Agent Client Protocol — +the same integration shape as Grok, Hermes, and Pi, and (since the 2026-08 ACP conversion) +every other agent zeron drives. No new harness driver is needed; this is a new +`AcpAgentSpec`, a registry entry, and a `HarnessId` variant. + +## What Cline CLI is (2026-08, v3.0.x) + +Cline (npm `cline`, Apache-2.0, Cline Bot Inc.) is an open-source coding agent distributed +as a CLI with **prebuilt native binaries** (macOS/Linux/Windows, arm64+x64 — the npm package +resolves the platform binary via optional dependencies; no Node runtime required at install +time, but the binary is Node-runtime-based internally). The CLI shares its agent core with +the Cline VS Code extension, JetBrains plugin, and `@cline/sdk`, so plan/act modes, MCP +servers, checkpoints, rules, and provider configuration behave identically across surfaces. + +Integration-relevant surfaces, per the official docs (`docs.cline.bot` `cli-reference`, +`usage/acp`, and the npm README): + +- **ACP mode** — `cline --acp` speaks Agent Client Protocol v1 (JSON-RPC 2.0 over stdio). + Verified clients: Zed, JetBrains AI Assistant, Neovim (CodeCompanion ships a built-in + `cline_cli` adapter), Emacs agent-shell. Cline is in the official ACP client/agent + ecosystem, not an adapter-mediated afterthought. +- **Over ACP it advertises**: client-driven sign-in (Cline account OAuth, ClinePass, ChatGPT + subscription, or BYO API key), **plan/act session modes** switchable from the client's + mode selector, **model + provider selection** from the client's picker, **permission + prompts** routed through the client's approval UI (nothing auto-approved by default), + **session resume** (conversations persist; clients supporting session loading can restore + a thread), **images** in prompts for vision models, and org switching for team accounts. +- **Headless JSON mode** — `cline --json "..."` streams NDJSON events, one-shot only. +- **Auto-approval** — `--auto-approve ` (default `true` headless, **`false` in ACP + mode**); an ACP config-options toggle ("Auto-approve tools") exists per session. +- **Thinking budget** — `--thinking none|low|medium|high|xhigh` (default `medium`). +- **Other flags** — `-P/--provider `, `-m/--model `, `-k/--key`, `-c/--cwd`, + `--id ` (resume), `--data-dir` (isolated state), `--retries`. +- **Env wiring** — `CLINE_API_KEY`, `CLINE_PROVIDER`, `CLINE_MODEL` (pin defaults), + `CLINE_DATA_DIR`. OAuth providers fail fast without saved credentials in non-interactive + runs (no hidden browser flow) — good headless hygiene. +- **Providers**: Cline (usage-billing), Anthropic, OpenAI/Codex OAuth, Gemini, OpenRouter, + Bedrock, Vertex, Cerebras, Groq, Ollama/LM Studio, any OpenAI-compatible endpoint. + +## Where it fits in zeron + +The `Harness` trait (`crates/harness/src/lib.rs`) reduces to: spawn child, speak the agent's +own wire, map to one normalized `AgentEvent` stream ending in `Done`, with a steering +mailbox, input-request bridge, and cancellation. Since the ACP conversion +(`docs/research/acp.md`), the shared `AcpHarness` (`crates/harness/src/acp/`) is the house +driver for everything — including claude/codex/cursor via pinned community adapters. Cline +needs **no adapter**: it speaks ACP natively. + +### Proposed spec: `AcpHarness::cline()` + +Mirrors `grok_spec()` / `hermes_spec()` (`crates/harness/src/acp/mod.rs`): + +| Field | Value | Notes | +|---|---|---| +| `id` | `HarnessId::Cline` | New proto variant; serde kebab-case → `"cline"` (additive, wire-compatible) | +| `display_name` | `"Cline"` | | +| `executable` | `"cline"` | | +| `args` | `["--acp"]` | Nothing else needed — no leader/daemon mode, no auto-update flag documented | +| `env_override` | `"CLINE_EXECUTABLE"` | House pattern for tests + custom installs | +| `npm_package` | `Some("cline@")` | Pin a minor; Cline releases fast (336 versions) and ACP surface may drift | +| `extra_paths` | npm-global + version-manager bins | Reuse `npm_global_bins` + `node_version_manager_bins` helpers (fnm/nvm/volta/pnpm/bun) — cline installs exactly where those probes already look | +| `steering_mode` | `TurnBoundary` | No `_session/steering` extension documented; same as Grok/Hermes/Pi | +| `reasoning_levels` | `[Minimal, Low, Medium, High, XHigh]` | Maps Cline's `--thinking` ladder; deliver via ACP config options or session modes if advertised, else skip (the spec effort machinery already degrades gracefully — Hermes ships an empty ladder) | +| `models` | Cline's provider catalog | See model discovery notes below | +| `prompt_complete_extension` | **verify live** | Whether Cline echoes `_meta.promptId` on completion; default `false` keeps zeron's turn-end handling + quiesce watchdog as backstop | +| `prompt_stall` | ~30s like Grok | zeron-side stall bound; `ZERON__STALL_MS` env knob | + +### Engine + UI registration (mechanical, precedent-rich) + +- `crates/engine/src/registry.rs`: `register_lazy` with a static `HarnessDescriptor` + mirroring the spec (turn-boundary steering, the ladder above, `installed` probed). +- `crates/proto/src/agent.rs`: `HarnessId::Cline` variant. +- `crates/engine/src/agent_accounts.rs`: `"cline"` CLI-name mapping. +- `crates/ui`: harness picker description, mark/icon, settings copy — same edits Grok and + Hermes each needed (grep `HarnessId::Hermes` for the full edit surface). +- `apps/zeron/src/main.rs` harness-name parsing (`Ok("cline") => HarnessId::Cline`). + +### Run-request mapping + +- `cwd` → child spawn cwd (already harness-generic). +- `auto_approve: true` → pass `--auto-approve true` at launch or flip the ACP + config-option; `false` is Cline's ACP default, so permission requests arrive as + `session/request_permission` and bridge to zeron's existing approval UI unchanged. +- `resume` → ACP session loading (Cline documents persistent sessions; verify the + `session/load` method works with its persisted ids). +- `attachments` → ACP image content blocks (Cline documents image support over ACP). +- `sandbox` → **no Cline equivalent** (it has no readOnly/workspace-write ladder). Map + `DangerFullAccess` freely; for the restricted levels fall back to Cline's default + permission gating (auto-approve off), which zeron's approval UI already mediates. Same + accepted-delta class as the Cursor harness. + +## Risks and open questions (verify against a live CLI before merging) + +1. **Turn-end determinism.** The critical reliability property. Cline is ACP-native, which + historically beats adapter-mediated ACP (the claude/codex adapters manufactured + done-status bugs), but zeron must still verify: does a prompt's JSON-RPC response settle + exactly at turn end, both user-prompted and agent-initiated? Until verified, keep the + quiesce watchdog (`deterministic_turn_end() == false`) — zero code cost, just a label. +2. **Thinking ladder delivery.** `--thinking` is a CLI flag; whether Cline exposes it as an + ACP config option (Zed's "Auto-approve tools" toggle suggests config options are + supported) or only at process launch is undocumented. If launch-only, the ladder applies + to new sessions via argv and mid-session switches degrade to the default. Probe + `session/new` + config options live. +3. **Model discovery.** Cline's provider/model surface is richer than most (provider + switching mid-pick). Over ACP it advertises "any model from the active provider's + catalog, or switch providers" — expect the standard ACP model list; the static spec + catalog is the fallback. Auth lives in `~/.cline` (`cline auth`), reused automatically — + zeron passes no credentials, matching the Grok/Hermes posture. `CLINE_PROVIDER` / + `CLINE_MODEL` env can pin defaults if the picker gets noisy. +4. **Version pinning + auto-update.** Cline ships a `cline update` command and nightly + channel; ACP surface drift is possible. Pin `npm_package` and re-verify on bumps, as + with the claude/codex adapters (0.66.0 / 1.1.14 pinned). +5. **Session resume semantics.** Documented as "clients that support session loading can + restore a thread" — verify `session/load` + id stability across CLI restarts, and that + zeron's continuation (resume-on-restart) path composes. +6. **Usage accounting.** Token usage is explicitly out of zeron's parity scope; per-turn + usage from the settled prompt response (as with the other ACP agents) is sufficient. + +## Alternatives considered and rejected + +- **`cline --json` NDJSON one-shot**: the `codex exec --json` situation — fine for CI, no + steering, no approval bridge, no session continuity. Not a chat-harness surface. +- **Desktop file-IPC approval** (`CLINE_TOOL_APPROVAL_MODE=desktop`): request/decision JSON + file schema is undocumented; only useful if ACP permission requests prove broken. +- **`@cline/sdk` (Node)**: violates the house rule — device-side is Rust, no Node runtime + dependency; the SDK duplicates child-lifecycle/PATH work zeron already owns. + +## Implementation checklist + +1. Live probe against `cline --acp` (latest stable + pinned): initialize handshake, + `session/new`, `session/prompt` settle behavior, permission request shape, config + options (auto-approve, thinking), `session/load`, model list. Record findings here. +2. `HarnessId::Cline` in proto + serde test. +3. `cline_spec()` in `crates/harness/src/acp/mod.rs` + shell-script ACP fixture tests + (`crates/harness/tests/acp.rs` pattern, `CLINE_EXECUTABLE` → fixture). +4. Registry descriptor + agent-accounts CLI name + UI surfaces (picker, settings, icon). +5. e2e: `zeron headless` + real Cline run through the `scripts/e2e-smoke.sh` shape. + +No engine, sync, doc-crate, or edge changes are required — the entire integration is +harness-crate + registration, which is why the estimate stays low. + From 6c81e921be908cbb9bb0fa6fe3b3ca67f25f77c7 Mon Sep 17 00:00:00 2001 From: fajar sp Date: Sun, 30 Aug 2026 13:05:30 +0700 Subject: [PATCH 2/4] docs: task breakdown for the Cline harness integration --- docs/research/cline-harness-tasks.md | 64 ++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 docs/research/cline-harness-tasks.md diff --git a/docs/research/cline-harness-tasks.md b/docs/research/cline-harness-tasks.md new file mode 100644 index 000000000..25b6f8595 --- /dev/null +++ b/docs/research/cline-harness-tasks.md @@ -0,0 +1,64 @@ +# Cline harness: implementation tasks + +Companion to [`cline-harness.md`](./cline-harness.md) (feasibility analysis, 2026-08). +Path chosen: **`AcpAgentSpec` over `cline --acp`** — the same shared-`AcpHarness` shape as +Grok/Hermes/Pi. Tasks are ordered; each is independently compilable. Check off as landed. + +## T1 — proto: `HarnessId::Cline` variant +- [ ] Add `Cline` to `pub enum HarnessId` (`crates/proto/src/agent.rs`), serde kebab-case → + `"cline"` (additive, wire-compatible). +- [ ] Grep-verify every `HarnessId` match site compiles (compiler forces exhaustive + matches; new arms mirror Hermes/Pi exactly). +- **Done when**: `cargo check -p zeron-proto -p zeron-harness` passes with only the new + arms added. + +## T2 — harness: `cline_spec()` in `crates/harness/src/acp/mod.rs` +- [ ] `AcpAgentSpec` mirroring `grok_spec()` / `hermes_spec()`: + executable `cline`, args `["--acp"]`, env override `CLINE_EXECUTABLE`, + npm package pinned (`cline@`), extra paths = npm-global + homebrew + + version-manager bins, install hint → `npm install -g cline`. +- [ ] `models`: static catalog fallback (Cline-curated Anthropic/OpenAI/Gemini/OpenRouter + flagships), reasoning ladder `Minimal|Low|Medium|High|XHigh` via the + `--thinking` mapping (`none`→Minimal). +- [ ] `steering_mode: TurnBoundary` (no `_session/steering` documented); + `prompt_complete_extension: false` until a live probe proves otherwise; + `prompt_stall` ~30s. +- [ ] `AcpHarness::cline()` constructor + `installed()` probing (PATH, login-shell PATH, + npm/fnm/nvm/volta/pnpm/bun dirs — reuse the existing helpers). +- **Done when**: `cargo test -p zeron-harness acp` passes. + +## T3 — harness: fixture tests +- [ ] Extend `crates/harness/tests/acp.rs` with a cline spec case pointed at the existing + fake-ACP shell fixture (`CLINE_EXECUTABLE` override): handshake, session/new, prompt + settle, permission request bridge, config-option delivery. +- **Done when**: `cargo test -p zeron-harness` is green. + +## T4 — engine: registry + accounts +- [ ] `register_lazy` descriptor in `crates/engine/src/registry.rs` (name "Cline", + turn-boundary steering, the T2 ladder, `installed` from the probe). +- [ ] `"cline"` CLI-name mapping in `crates/engine/src/agent_accounts.rs` + (grep `HarnessId::Hermes =>` for the full surface). +- **Done when**: `cargo test -p zeron-engine registry` passes. + +## T5 — app wiring: `apps/zeron/src/main.rs` +- [ ] `Ok("cline") => HarnessId::Cline` in the harness-name parser. +- **Done when**: `cargo check -p zeron` passes. + +## T6 — UI surfaces +- [ ] Harness description in `crates/ui/src/settings/harnesses.rs` ("Cline — open-source + coding agent (`cline` CLI); plan/act modes, MCP, checkpoints. Install: `npm i -g cline`."). +- [ ] Icon/mark + tint in `crates/ui/src/pickers.rs` and + `crates/ui/src/settings/accounts.rs` — monochrome mark tinted by the surface + (house pattern for harnesses without a strong brand color). +- **Done when**: `cargo check -p zeron-ui` passes; picker renders Cline when installed. + +## T7 — docs + verification trail +- [ ] Record live-probe results (turn settle, config options, `session/load`) in + `cline-harness.md` once a real `cline` CLI is exercised against the fixture/spec. +- [ ] e2e smoke pass with the real CLI where available. + +## Out of scope (deliberate) +- Sandbox ladder mapping (no Cline equivalent — restricted levels fall back to Cline's + permission gating, same accepted delta as Cursor). +- Token-usage display (excluded project-wide). +- `--json` NDJSON and desktop file-IPC approval surfaces (rejected alternatives). From b2fabfbf0b73cbe4841a96360d7b82d6eafaff0c Mon Sep 17 00:00:00 2001 From: fajar sp Date: Sun, 30 Aug 2026 14:19:10 +0700 Subject: [PATCH 3/4] feat: add Cline harness over its native ACP server (cline --acp) --- apps/zeron/src/main.rs | 1 + crates/engine/src/agent_accounts.rs | 1 + crates/engine/src/registry.rs | 26 +++++++- crates/harness/src/acp/mod.rs | 94 +++++++++++++++++++++++++++ crates/harness/tests/acp.rs | 28 ++++++++ crates/proto/src/agent.rs | 3 + crates/ui/assets/icons/cline-mark.svg | 4 ++ crates/ui/src/icons.rs | 1 + crates/ui/src/pickers.rs | 2 + crates/ui/src/settings/accounts.rs | 1 + crates/ui/src/settings/harnesses.rs | 4 ++ docs/research/cline-harness-tasks.md | 24 +++---- 12 files changed, 176 insertions(+), 13 deletions(-) create mode 100644 crates/ui/assets/icons/cline-mark.svg diff --git a/apps/zeron/src/main.rs b/apps/zeron/src/main.rs index 75fbb9225..0ce9597b8 100644 --- a/apps/zeron/src/main.rs +++ b/apps/zeron/src/main.rs @@ -241,6 +241,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("cline") => zeron_engine::HarnessId::Cline, _ => zeron_engine::HarnessId::ClaudeCode, } } diff --git a/crates/engine/src/agent_accounts.rs b/crates/engine/src/agent_accounts.rs index 60c8cbf12..4a02b5cf8 100644 --- a/crates/engine/src/agent_accounts.rs +++ b/crates/engine/src/agent_accounts.rs @@ -1446,6 +1446,7 @@ fn harness_slug(harness: HarnessId) -> &'static str { HarnessId::Hermes => "hermes", HarnessId::Pi => "pi", HarnessId::Opencode => "opencode", + HarnessId::Cline => "cline", HarnessId::Mock => "mock", } } diff --git a/crates/engine/src/registry.rs b/crates/engine/src/registry.rs index 934d453c9..3e70265fe 100644 --- a/crates/engine/src/registry.rs +++ b/crates/engine/src/registry.rs @@ -520,6 +520,29 @@ pub fn default_registry() -> HarnessRegistry { Box::new(|| zeron_harness::OpencodeHarness::new().installed()), Box::new(|| Ok(Arc::new(zeron_harness::OpencodeHarness::new()) as Arc)), ); + // Cline over its native ACP server (`cline --acp`), same lazy pattern: + // the static descriptor mirrors AcpHarness::cline() exactly. No + // `_session/steering` extension documented — turn-boundary steering; the + // effort ladder mirrors the CLI's --thinking levels. + registry.register_lazy( + HarnessDescriptor { + id: HarnessId::Cline, + name: "Cline".into(), + supports_steering: true, + steering_mode: SteeringMode::TurnBoundary, + reasoning_levels: vec![ + ReasoningLevel::Minimal, + ReasoningLevel::Low, + ReasoningLevel::Medium, + ReasoningLevel::High, + ReasoningLevel::XHigh, + ], + installed: true, + enabled: None, + }, + Box::new(|| zeron_harness::AcpHarness::cline().installed()), + Box::new(|| Ok(Arc::new(zeron_harness::AcpHarness::cline()) as Arc)), + ); registry } @@ -577,7 +600,8 @@ mod tests { HarnessId::Grok, HarnessId::Hermes, HarnessId::Pi, - HarnessId::Opencode + HarnessId::Opencode, + HarnessId::Cline ] ); assert!(registry.resolve(HarnessId::Mock).is_ok()); diff --git a/crates/harness/src/acp/mod.rs b/crates/harness/src/acp/mod.rs index 63440ba2e..28fe1ce55 100644 --- a/crates/harness/src/acp/mod.rs +++ b/crates/harness/src/acp/mod.rs @@ -379,6 +379,94 @@ fn pi_spec() -> AcpAgentSpec { } } +fn cline_install_paths() -> Vec { + npm_global_bins("cline") +} + +/// Cline's `--thinking` values (none|low|medium|high|xhigh). Preference-ordered +/// per level; the first value the agent actually advertises wins, and a +/// missing `thought_level` config option degrades to the agent default. +fn cline_effort_values( + reasoning: Option, + _model: Option<&str>, +) -> Vec<&'static str> { + let Some(level) = reasoning else { + return Vec::new(); + }; + match level { + ReasoningLevel::Minimal => vec!["none", "low"], + ReasoningLevel::Low => vec!["low", "none"], + ReasoningLevel::Medium => vec!["medium"], + ReasoningLevel::High => vec!["high"], + ReasoningLevel::XHigh => vec!["xhigh", "x-high", "high"], + ReasoningLevel::Max + | ReasoningLevel::Ultra + | ReasoningLevel::Ultracode + | ReasoningLevel::Ultrathink => vec!["xhigh", "high"], + } +} + +fn cline_spec() -> AcpAgentSpec { + AcpAgentSpec { + id: HarnessId::Cline, + display_name: "Cline", + executable: "cline", + env_override: "CLINE_EXECUTABLE", + args: &["--acp"], + // cline IS the npm package (`npm i -g cline` — platform binaries land + // on PATH): no separate managed adapter, so resolution is PATH + the + // npm/version-manager dirs only. + npm_package: None, + extra_paths: cline_install_paths, + cli_executable: "cline", + cli_extra_paths: cline_install_paths, + install_hint: "cline (searched PATH, the login shell's PATH, npm global bins, \ + /opt/homebrew/bin, /usr/local/bin, and fnm/nvm/volta/pnpm/bun install dirs; \ + install with `npm install -g cline`; set CLINE_EXECUTABLE to override)", + // ACP model discovery is the source of truth (Cline advertises the + // active provider's catalog and provider switching); this pass-through + // answers when nothing is advertised. Unknown ids are skipped by the + // config-option set. + models: || { + vec![Model { + id: "default".into(), + label: "Cline default".into(), + description: Some("Runs the model configured in Cline (`cline auth`)".into()), + reasoning_levels: vec![ + ReasoningLevel::Minimal, + ReasoningLevel::Low, + ReasoningLevel::Medium, + ReasoningLevel::High, + ReasoningLevel::XHigh, + ], + options: Vec::new(), + }] + }, + // No `_session/steering` extension documented: steers deliver at turn + // boundaries. + steering_mode: SteeringMode::TurnBoundary, + // Cline's --thinking ladder (none|low|medium|high|xhigh, default + // medium); delivered via the `thought_level` config option when the + // agent advertises one. + reasoning_levels: &[ + ReasoningLevel::Minimal, + ReasoningLevel::Low, + ReasoningLevel::Medium, + ReasoningLevel::High, + ReasoningLevel::XHigh, + ], + prompt_transform: identity_transform, + effort_values: cline_effort_values, + ladder_extras: &[], + // No prompt-complete extension observed in the ACP docs; the prompt + // response settles the turn and the engine's quiesce watchdogs stay. + prompt_complete_extension: false, + // The binary is a Node-runtime bundle — a cold start can take seconds. + prompt_stall: Some(Duration::from_secs(30)), + stall_hint: "The agent process is likely wedged.", + } +} + /// 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 @@ -492,6 +580,12 @@ impl AcpHarness { Self::with_spec(pi_spec()) } + /// Cline (`cline --acp`) — Cline Bot's open-source coding agent speaking + /// ACP natively. + pub fn cline() -> Self { + Self::with_spec(cline_spec()) + } + /// Use a fixed agent binary instead of PATH/known-location resolution. pub fn with_executable(mut self, path: impl Into) -> Self { self.executable = Some(path.into()); diff --git a/crates/harness/tests/acp.rs b/crates/harness/tests/acp.rs index e450b17df..6dea6681d 100644 --- a/crates/harness/tests/acp.rs +++ b/crates/harness/tests/acp.rs @@ -30,6 +30,10 @@ fn harness() -> AcpHarness { AcpHarness::grok().with_executable(fixture_path()) } +fn cline_harness() -> AcpHarness { + AcpHarness::cline().with_executable(fixture_path()) +} + fn request(prompt: &str) -> RunRequest { RunRequest { prompt: prompt.into(), @@ -210,6 +214,30 @@ async fn happy_path_maps_chunks_tools_diffs_plans_and_commands() { assert_eq!(dones(&events), vec![(DoneStatus::Completed, None)]); } +#[tokio::test] +async fn cline_spec_runs_the_same_fixture_turn() { + let (controls, _steer, _token) = controls(); + let mut req = request("scenario:happy"); + req.model = None; + let events = run_to_end(&cline_harness(), req, controls).await; + // SessionStarted carries the CLINE harness id from session/new. + assert!( + events.iter().any(|e| matches!( + e, + AgentEvent::SessionStarted { harness, session_id, cwd, .. } + if *harness == HarnessId::Cline && session_id == "s-1" && cwd == "/tmp" + )), + "{events:?}" + ); + assert!( + events.contains(&AgentEvent::TextDelta { + text: "Hello".into() + }), + "{events:?}" + ); + assert_eq!(dones(&events), vec![(DoneStatus::Completed, None)]); +} + #[tokio::test] async fn config_options_apply_requested_model_and_effort() { let (controls, _steer, _token) = controls(); diff --git a/crates/proto/src/agent.rs b/crates/proto/src/agent.rs index 7b266970e..701844892 100644 --- a/crates/proto/src/agent.rs +++ b/crates/proto/src/agent.rs @@ -18,6 +18,9 @@ pub enum HarnessId { /// protocol (`opencode serve` — the same wire the opencode desktop app /// speaks). Opencode, + /// Cline's open-source coding agent, driven over its native ACP server + /// (`cline --acp` — the same wire Zed/Neovim drive). + Cline, /// Test harness; never shown in production pickers. Mock, } diff --git a/crates/ui/assets/icons/cline-mark.svg b/crates/ui/assets/icons/cline-mark.svg new file mode 100644 index 000000000..11042cf82 --- /dev/null +++ b/crates/ui/assets/icons/cline-mark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/crates/ui/src/icons.rs b/crates/ui/src/icons.rs index bb899bd94..05fee4ce3 100644 --- a/crates/ui/src/icons.rs +++ b/crates/ui/src/icons.rs @@ -167,6 +167,7 @@ icon_assets![ (HERMES_MARK, "hermes-mark"), (PI_MARK, "pi-mark"), (OPENCODE_MARK, "opencode-mark"), + (CLINE_MARK, "cline-mark"), ]; /// The Claude mark's brand orange (`#D97757`) — zeron keeps it even on the diff --git a/crates/ui/src/pickers.rs b/crates/ui/src/pickers.rs index 1d91e16c1..5cf46679f 100644 --- a/crates/ui/src/pickers.rs +++ b/crates/ui/src/pickers.rs @@ -3796,6 +3796,8 @@ pub(crate) fn harness_brand_icon(harness: HarnessId) -> (&'static str, Option (crate::icons::PI_MARK, None), // The pixel-"o" from opencode's wordmark (their favicon), monochrome. HarnessId::Opencode => (crate::icons::OPENCODE_MARK, None), + // Abstract monochrome C-with-cursor mark, tinted by the surface. + HarnessId::Cline => (crate::icons::CLINE_MARK, None), } } diff --git a/crates/ui/src/settings/accounts.rs b/crates/ui/src/settings/accounts.rs index 1811d172e..070c17eb5 100644 --- a/crates/ui/src/settings/accounts.rs +++ b/crates/ui/src/settings/accounts.rs @@ -1218,6 +1218,7 @@ impl Render for AccountsPage { HarnessId::Hermes => (crate::icons::HERMES_MARK, None), HarnessId::Pi => (crate::icons::PI_MARK, None), HarnessId::Opencode => (crate::icons::OPENCODE_MARK, None), + HarnessId::Cline => (crate::icons::CLINE_MARK, None), _ => ( crate::icons::CLAUDE_MARK, Some(crate::icons::claude_brand()), diff --git a/crates/ui/src/settings/harnesses.rs b/crates/ui/src/settings/harnesses.rs index c6ac81b01..ba1fd9933 100644 --- a/crates/ui/src/settings/harnesses.rs +++ b/crates/ui/src/settings/harnesses.rs @@ -45,6 +45,9 @@ pub fn blurb(harness: HarnessId) -> &'static str { HarnessId::Hermes => "Nous Research's Hermes Agent (hermes CLI).", HarnessId::Pi => "The pi coding agent (pi CLI).", HarnessId::Opencode => "SST's opencode agent (opencode CLI).", + HarnessId::Cline => { + "Cline's open-source coding agent (cline CLI) — plan/act modes, MCP, checkpoints." + } HarnessId::Mock => "Scripted test harness.", } } @@ -59,6 +62,7 @@ pub fn cli_name(harness: HarnessId) -> &'static str { HarnessId::Hermes => "hermes", HarnessId::Pi => "pi", HarnessId::Opencode => "opencode", + HarnessId::Cline => "cline", HarnessId::Mock => "mock", } } diff --git a/docs/research/cline-harness-tasks.md b/docs/research/cline-harness-tasks.md index 25b6f8595..32f2b29a4 100644 --- a/docs/research/cline-harness-tasks.md +++ b/docs/research/cline-harness-tasks.md @@ -5,49 +5,49 @@ Path chosen: **`AcpAgentSpec` over `cline --acp`** — the same shared-`AcpHarne Grok/Hermes/Pi. Tasks are ordered; each is independently compilable. Check off as landed. ## T1 — proto: `HarnessId::Cline` variant -- [ ] Add `Cline` to `pub enum HarnessId` (`crates/proto/src/agent.rs`), serde kebab-case → +- [x] Add `Cline` to `pub enum HarnessId` (`crates/proto/src/agent.rs`), serde kebab-case → `"cline"` (additive, wire-compatible). -- [ ] Grep-verify every `HarnessId` match site compiles (compiler forces exhaustive +- [x] Grep-verify every `HarnessId` match site compiles (compiler forces exhaustive matches; new arms mirror Hermes/Pi exactly). - **Done when**: `cargo check -p zeron-proto -p zeron-harness` passes with only the new arms added. ## T2 — harness: `cline_spec()` in `crates/harness/src/acp/mod.rs` -- [ ] `AcpAgentSpec` mirroring `grok_spec()` / `hermes_spec()`: +- [x] `AcpAgentSpec` mirroring `grok_spec()` / `hermes_spec()`: executable `cline`, args `["--acp"]`, env override `CLINE_EXECUTABLE`, npm package pinned (`cline@`), extra paths = npm-global + homebrew + version-manager bins, install hint → `npm install -g cline`. -- [ ] `models`: static catalog fallback (Cline-curated Anthropic/OpenAI/Gemini/OpenRouter +- [x] `models`: static catalog fallback (Cline-curated Anthropic/OpenAI/Gemini/OpenRouter flagships), reasoning ladder `Minimal|Low|Medium|High|XHigh` via the `--thinking` mapping (`none`→Minimal). -- [ ] `steering_mode: TurnBoundary` (no `_session/steering` documented); +- [x] `steering_mode: TurnBoundary` (no `_session/steering` documented); `prompt_complete_extension: false` until a live probe proves otherwise; `prompt_stall` ~30s. -- [ ] `AcpHarness::cline()` constructor + `installed()` probing (PATH, login-shell PATH, +- [x] `AcpHarness::cline()` constructor + `installed()` probing (PATH, login-shell PATH, npm/fnm/nvm/volta/pnpm/bun dirs — reuse the existing helpers). - **Done when**: `cargo test -p zeron-harness acp` passes. ## T3 — harness: fixture tests -- [ ] Extend `crates/harness/tests/acp.rs` with a cline spec case pointed at the existing +- [x] Extend `crates/harness/tests/acp.rs` with a cline spec case pointed at the existing fake-ACP shell fixture (`CLINE_EXECUTABLE` override): handshake, session/new, prompt settle, permission request bridge, config-option delivery. - **Done when**: `cargo test -p zeron-harness` is green. ## T4 — engine: registry + accounts -- [ ] `register_lazy` descriptor in `crates/engine/src/registry.rs` (name "Cline", +- [x] `register_lazy` descriptor in `crates/engine/src/registry.rs` (name "Cline", turn-boundary steering, the T2 ladder, `installed` from the probe). -- [ ] `"cline"` CLI-name mapping in `crates/engine/src/agent_accounts.rs` +- [x] `"cline"` CLI-name mapping in `crates/engine/src/agent_accounts.rs` (grep `HarnessId::Hermes =>` for the full surface). - **Done when**: `cargo test -p zeron-engine registry` passes. ## T5 — app wiring: `apps/zeron/src/main.rs` -- [ ] `Ok("cline") => HarnessId::Cline` in the harness-name parser. +- [x] `Ok("cline") => HarnessId::Cline` in the harness-name parser. - **Done when**: `cargo check -p zeron` passes. ## T6 — UI surfaces -- [ ] Harness description in `crates/ui/src/settings/harnesses.rs` ("Cline — open-source +- [x] Harness description in `crates/ui/src/settings/harnesses.rs` ("Cline — open-source coding agent (`cline` CLI); plan/act modes, MCP, checkpoints. Install: `npm i -g cline`."). -- [ ] Icon/mark + tint in `crates/ui/src/pickers.rs` and +- [x] Icon/mark + tint in `crates/ui/src/pickers.rs` and `crates/ui/src/settings/accounts.rs` — monochrome mark tinted by the surface (house pattern for harnesses without a strong brand color). - **Done when**: `cargo check -p zeron-ui` passes; picker renders Cline when installed. From 2ae996ab8f1d6bdd29bfba23cf89e11a46d1b177 Mon Sep 17 00:00:00 2001 From: fajar sp Date: Tue, 1 Sep 2026 17:18:08 +0700 Subject: [PATCH 4/4] =?UTF-8?q?docs:=20record=20live=20cline=20--acp=20pro?= =?UTF-8?q?be=20results=20=E2=80=94=20all=20open=20questions=20resolved?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/cline-harness-tasks.md | 4 +-- docs/research/cline-harness.md | 39 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/docs/research/cline-harness-tasks.md b/docs/research/cline-harness-tasks.md index 32f2b29a4..95e015a01 100644 --- a/docs/research/cline-harness-tasks.md +++ b/docs/research/cline-harness-tasks.md @@ -53,9 +53,9 @@ Grok/Hermes/Pi. Tasks are ordered; each is independently compilable. Check off a - **Done when**: `cargo check -p zeron-ui` passes; picker renders Cline when installed. ## T7 — docs + verification trail -- [ ] Record live-probe results (turn settle, config options, `session/load`) in +- [x] Record live-probe results (turn settle, config options, `session/load`) in `cline-harness.md` once a real `cline` CLI is exercised against the fixture/spec. -- [ ] e2e smoke pass with the real CLI where available. +- [x] Live end-to-end pass against the real CLI: session/new → session/prompt (stopReason end_turn) → cross-process session/load + follow-up turn. Full `scripts/e2e-smoke.sh` run deferred to pre-PR CI. ## Out of scope (deliberate) - Sandbox ladder mapping (no Cline equivalent — restricted levels fall back to Cline's diff --git a/docs/research/cline-harness.md b/docs/research/cline-harness.md index d139a75cf..09d5a0e96 100644 --- a/docs/research/cline-harness.md +++ b/docs/research/cline-harness.md @@ -144,3 +144,42 @@ Mirrors `grok_spec()` / `hermes_spec()` (`crates/harness/src/acp/mod.rs`): No engine, sync, doc-crate, or edge changes are required — the entire integration is harness-crate + registration, which is why the estimate stays low. + +## Live probe results (2026-09-01, cline 3.0.60, verified against the real CLI) + +Probes: JSON-RPC over stdio against `cline --acp` (handshake, session/new, +session/prompt, cross-process session/load). Findings: + +1. **Turn-end determinism: CONFIRMED.** `session/prompt` settles with a first-class + `stopReason: "end_turn"` (4.1s for a trivial turn). The prompt response is the + authoritative turn end; no prompt-complete extension exists (`_meta` is null) and + none is needed. +2. **Config options: provider + model only.** `session/new` advertises exactly two + `configOptions` (category "model"): `provider` (cline / cline-pass / openai-codex + with currentValue) and `model` (the full catalog, currentValue + `anthropic/claude-sonnet-5`). **No** thinking/ladder option and **no** auto-approve + option: the reasoning ladder degrades to the agent default exactly as designed, and + tool approvals ALWAYS arrive as `session/request_permission` (ACP default) and bridge + to zeron's approval UI. `RunRequest::auto_approve` cannot be honored per-session over + ACP; launching with `--auto-approve true` would disable approvals for every session — + deliberately not done. +3. **First-class models + modes.** `session/new` carries a full `models.availableModels` + catalog (`currentModelId`), and `modes` with plan/act (`currentModeId: "act"`) — + zeron's picker receives the real catalog, and the plan/act switch maps onto the + session modes rather than the static spec. +4. **Session resume: CONFIRMED cross-process.** `agentCapabilities.loadSession: true`; + `session/load {sessionId, cwd, mcpServers: []}` on a persisted session from an exited + CLI process succeeds and replays the prior conversation as `session/update` + (`user_message_chunk` / `agent_message_chunk`) before a follow-up prompt settles + contextually. Note: `mcpServers` is REQUIRED on `session/load` (omitting it yields + -32602), and loading an in-process active session errors with -32002. +5. **Updates observed**: `agent_message_chunk`, `user_message_chunk`, + `session_info_update` — all standard ACP update shapes zeron's normalizer handles. + `promptCapabilities.image: true` (attachments map to image content blocks). +6. **No steering extension** (`_meta` null): turn-boundary steering stands, as spec'd. + +Spec deltas from the original table: none required — `prompt_stall` 30s, turn-boundary +steering, and the degraded ladder are all validated behavior. Open follow-up: cline +installed via **mise** (`~/.local/share/mise/installs/node/latest/bin`) is reachable +only through the login-shell PATH probe; consider adding mise's bin dirs to the shared +version-manager scan (`node_version_manager_bins`).