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
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ endif
eval-core-test litellm-up litellm-down stack-up stack-down stack-status \
openrouter-smoke env-check \
harness-image-aider harness-image-goose harness-image-opencode \
harness-image-crush sandbox-net-up
harness-image-crush harness-image-cline sandbox-net-up

demo-run:
python -m pollmevals_eval_core.demo_run --tasks evals/tasks --output artifacts
Expand Down Expand Up @@ -134,6 +134,11 @@ harness-image-opencode:
harness-image-crush:
docker build -t pollmevals-harness-crush:0.1.0 infra/docker/harness-crush/

# Build the cline harness image (RFC-006 Phase 5). No spend — node base + npm
# cline. Model-agnostic via cline's openai-native provider + base-URL override.
harness-image-cline:
docker build -t pollmevals-harness-cline:0.1.0 infra/docker/harness-cline/

# Build the Python eval sandbox image (Phase 5 — PythonCorrectnessEvaluator,
# runs BigCodeBench unittest suites). No spend — pulls base + pip scientific stack.
eval-image-py:
Expand Down
4 changes: 4 additions & 0 deletions apps/eval-core-py/scripts/build_real_board.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,16 @@
_OPENCODE_MODELS = ["qwen-3-14b", "qwen3-coder-30b", "codestral", "devstral"]
# Crush (charmbracelet): model-agnostic, same coder models for a clean comparison.
_CRUSH_MODELS = ["qwen-3-14b", "qwen3-coder-30b", "codestral", "devstral"]
# Cline: model-agnostic; same coder models (qwen-3-14b is too weak for its
# tool-use — a real compat data point; the stronger coders work).
_CLINE_MODELS = ["qwen-3-14b", "qwen3-coder-30b", "codestral", "devstral"]
# Per-stack candidate model lists for --add-stack (merge ONE harness column in).
_STACK_MODELS = {
"aider": _AIDER_MODELS,
"goose": _GOOSE_MODELS,
"opencode": _OPENCODE_MODELS,
"crush": _CRUSH_MODELS,
"cline": _CLINE_MODELS,
}
_SEEDS = [1, 2]
_TASK = "be_01_jwt_auth"
Expand Down
44 changes: 40 additions & 4 deletions apps/eval-core-py/src/orchestrator/stack_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,14 +348,51 @@ def _crush_invocation(
)


# Proven recipes (validated end-to-end via the proxy). aider is the RFC-006
# first slice (aider x qwen x be_01); goose / opencode / crush (all 2026-06-03)
# are model-agnostic peers that run the same coder models for a clean comparison.
def _cline_invocation(
proxy_base_url: str, api_key: str, model_alias: str, prompt: str
) -> ProxyInvocation:
"""Cline recipe — PROVEN (2026-06-03 isolation smoke; memory).

Cline is model-agnostic over any OpenAI-compatible endpoint, but UNIQUELY a
custom base URL is NOT an env var and NOT a run flag — it is seeded only via
``cline auth --provider openai-native --baseurl <proxy>/v1``, which persists to
the data dir (kept in /tmp by the image's CLINE_DATA_DIR, out of /workspace).
So the run is a two-step ``sh -c``: auth (seed) then ``cline --yolo`` (the
one-shot, auto-approve all tools). Cline edits the real cwd (/workspace); its
file writes are captured by the host git-diff. Two quirks handled here:
* ``openai-native`` is the correct provider id for a generic OpenAI gateway
(NOT ``openai-compatible``, whose CLI wizard is buggy — cline #9656).
* Cline's exit code is unreliable (a post-submission "verify" call can hit
an upstream Responses-API quirk and exit non-zero AFTER the file is
written), so the wrapper ends ``|| true`` — the produced PATCH is the real
signal (empty diff -> NO_PATCH, non-empty -> OK), not Cline's exit code.
The key is the literal ``$LITELLM_MASTER_KEY`` expanded by the in-container
shell, so it is never written into a file.
"""
base = proxy_base_url.rstrip("/")
safe_prompt = prompt.replace("'", "'\"'\"'") # single-quote-safe for the shell
wrapper = (
'cline auth --provider openai-native --apikey "$LITELLM_MASTER_KEY" '
f"--modelid {model_alias} --baseurl {base}/v1 && "
f"cline --yolo '{safe_prompt}' || true"
)
return ProxyInvocation(
env={"LITELLM_MASTER_KEY": api_key},
config_files={}, # cline writes its own providers.json (in /tmp) via auth
extra_args=[],
prompt_args=["-c", wrapper], # rides `sh` (stack.yaml command = "sh")
)


# Proven recipes (validated end-to-end via the proxy). aider is the RFC-006 first
# slice; goose / opencode / crush / cline (all 2026-06-03) are model-agnostic peers
# that run the same coder models for a clean harness comparison.
_PROVEN_RECIPES: dict[str, _RecipeBuilder] = {
"aider": _aider_invocation,
"goose": _goose_invocation,
"opencode": _opencode_invocation,
"crush": _crush_invocation,
"cline": _cline_invocation,
}

# Known harnesses whose recipe is proven in spikes but lands at its per-stack
Expand All @@ -366,7 +403,6 @@ def _crush_invocation(
"codex",
"openhands",
"hermes",
"cline",
"pi",
"forgeplan-framework",
}
Expand Down
23 changes: 21 additions & 2 deletions apps/eval-core-py/tests/test_stack_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,27 @@ def test_crush_recipe_is_proven(self) -> None:
assert prov["api_key"] == "$LITELLM_MASTER_KEY" # literal var; key not in file
assert prov["models"][0]["id"] == "qwen-3-14b"

def test_supported_harnesses_are_aider_goose_opencode_crush(self) -> None:
assert supported_harnesses() == frozenset({"aider", "goose", "opencode", "crush"})
def test_cline_recipe_is_proven(self) -> None:
inv = build_proxy_invocation(
"cline",
proxy_base_url="http://pollmevals-litellm-proxy:4000",
api_key="sk-local-xyz",
model_alias="qwen3-coder-30b",
prompt="do the thing",
)
assert inv.env["LITELLM_MASTER_KEY"] == "sk-local-xyz"
assert inv.config_files == {}
assert inv.prompt_args[0] == "-c"
wrapper = inv.prompt_args[1]
assert "cline auth --provider openai-native" in wrapper
assert '"$LITELLM_MASTER_KEY"' in wrapper # key referenced, not inlined
assert "--modelid qwen3-coder-30b" in wrapper
assert "--baseurl http://pollmevals-litellm-proxy:4000/v1" in wrapper
assert "cline --yolo 'do the thing'" in wrapper
assert wrapper.rstrip().endswith("|| true") # exit masked; the patch decides

def test_supported_harnesses_are_aider_goose_opencode_crush_cline(self) -> None:
assert supported_harnesses() == frozenset({"aider", "goose", "opencode", "crush", "cline"})


# ---------------------------------------------------------------------------
Expand Down
65 changes: 65 additions & 0 deletions infra/docker/harness-cline/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# syntax=docker/dockerfile:1.7
#
# pollmevals-harness-cline -- sandboxed Cline CLI image (RFC-006 Half A). Cline is
# a model-AGNOSTIC agentic CLI (it speaks any OpenAI-compatible endpoint via its
# `openai-native` provider + a base-URL override), so it runs the same open coder
# models as aider/goose/opencode/crush -- another "swap the harness" column.
#
# Candidate-side shape (same as the other harnesses, OPPOSITE of Half B):
# * /workspace WRITABLE (the harness edits files to produce a patch)
# * joins the `pollmevals-sandbox` INTERNAL net -> reaches ONLY the proxy.
#
# Pinned dependencies (Library-first; bump deliberately):
# node 22-slim (cline ships a Bun-compiled binary via npm optionalDeps
# @cline/cli-linux-<arch>; the `cline` launcher is a Node
# script -> Node base. Both arm64 + x64 publish at 3.0.15.)
# cline 3.0.15 (npm; pulls the platform binary, glibc -> node-slim)
# git (Debian stable) -- cline tracks edits; we capture the diff via git
#
# Cline headless gotchas (cline 3.0.15 README + docs.cline.bot):
# * A custom base URL is NOT an env var and NOT a run flag -- it is seeded only
# via `cline auth --provider openai-native --baseurl <proxy>/v1`, which
# persists to <data-dir>/settings/providers.json. So the run is a two-step
# sh -c: auth (seed) then the one-shot. Both share --data-dir /tmp/clinedata,
# placed OUTSIDE /workspace so providers.json + the session DB never land in
# the captured patch (same discipline as crush's CRUSH_GLOBAL_DATA=/tmp).
# * `--yolo` makes the run fully non-interactive (auto-approve all tools + exit).
# The provider/model/proxy/base-url is injected at run time by the recipe.
#
# Build: docker build -t pollmevals-harness-cline:0.1.0 infra/docker/harness-cline/
# (or: make harness-image-cline)
# Smoke: docker run --rm pollmevals-harness-cline:0.1.0 # -> prints cline version

FROM node:22-slim AS base

RUN apt-get update \
&& apt-get install -y --no-install-recommends git ca-certificates \
&& rm -rf /var/lib/apt/lists/*

# Install cline globally (pulls the platform binary via optionalDependencies:
# @cline/cli-linux-arm64 / @cline/cli-linux-x64, both present at 3.0.15). The
# binary is Bun-embedded/self-contained, so the no-egress run-time container needs
# no npm. Pinned.
RUN npm install -g --no-fund --no-audit cline@3.0.15 \
&& npm cache clean --force \
&& cline --version

# Non-root user: the node base already ships a uid-1000 `node` user, which matches
# the host bind owner so the produced patch is writable back.
USER node
WORKDIR /workspace

RUN git config --global user.email "harness@pollmevals.local" \
&& git config --global user.name "pollmevals-harness" \
&& git config --global init.defaultBranch main \
&& git config --global --add safe.directory /workspace

# Keep all cline state OUT of /workspace (providers.json + session DB live in
# /tmp so they never land in the captured patch). The recipe ALSO passes
# --data-dir /tmp/clinedata explicitly (authoritative); this is the matching default.
ENV CLINE_DATA_DIR=/tmp/clinedata \
CLINE_LOG_ENABLED=0

# Default command is a harmless version probe; DockerHarnessLauncher overrides
# `command` with the full `sh -c "cline auth ... && cline --yolo ..."` invocation.
CMD ["cline", "--version"]
12 changes: 11 additions & 1 deletion stacks/cline/stack.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ slug: cline
name: Cline
base_model_slug: configurable
agent_cli: cline
# Cline CLI: a system-prompted agent (L1) driving file-edit + shell tools (L2).
# Model-agnostic over any OpenAI-compatible endpoint. Recipe codified + smoked
# 2026-06-03 (_cline_invocation in stack_executor.py; memory: research-cli-harness-
# execution). UNIQUE shape — a custom base URL is seedable ONLY via `cline auth`
# (persists to the data dir, kept in /tmp), so the run is a two-step sh -c:
# sh -c 'cline auth --provider openai-native --apikey "$LITELLM_MASTER_KEY"
# --modelid <alias> --baseurl <proxy>/v1 && cline --yolo "<prompt>" || true'
# Edits land in the real /workspace (captured by git-diff); `|| true` masks Cline's
# noisy exit code (the produced patch is the real signal). command=sh, the full
# invocation rides prompt_args=["-c", <wrapper>].
layers:
L0_bare_llm: false
L1_system_prompt: true
Expand All @@ -15,7 +25,7 @@ layers:
L8_framework: null
execution:
mode: repository_patch
command: cline
command: sh # the full `cline auth && cline --yolo` rides prompt_args=["-c", wrapper]
args: []
input_contract:
receives:
Expand Down
Loading