Skip to content

Integrate Devin backend with ACP and update documentation - #34

Open
chenillen wants to merge 133 commits into
HarnessRouter:mainfrom
chenillen:feature/add_devin_harness_support
Open

Integrate Devin backend with ACP and update documentation#34
chenillen wants to merge 133 commits into
HarnessRouter:mainfrom
chenillen:feature/add_devin_harness_support

Conversation

@chenillen

Copy link
Copy Markdown

No description provided.

richard-epsilla and others added 30 commits August 19, 2026 14:37
…source adapter

Pi (earendil-works pi coding agent, MIT) becomes a backend the way the runner's header
always promised: one BACKENDS entry, a builder, a normalizer. `pi -p --mode json` emits a
JSONL event stream that maps cleanly onto the canonical claude stream-json — session header
to init, text deltas to assistant deltas with the codex-style self-healing tail on
message_end, tool_execution_start/end to tool_use/tool_result, agent_end to the result.

Two of pi's own behaviors carry real weight here:

  --session-id resumes a project session and CREATES it when the file is missing — the
  fresh-start fallback claude and codex each implement by hand is pi's documented default,
  so the resume path has no existence check to get wrong.

  The CLI exits 0 when the provider call fails. Verified against 0.84.2 with a bad key:
  the failure is stopReason="error" on the assistant message, nothing else. Status
  therefore derives from the synthesized result event, never the exit code, and the
  verbatim capture of that run is now a test fixture.

Providers mirror hermes's multi-family shape: native env auth for anthropic/openai, a
~/.pi/agent/models.json custom provider for anything with a base_url, api picked by model
family (claude -> anthropic-messages, gpt-5/codex -> openai-responses for the same reason
hermes routes them there, else openai-completions). The /v1 suffix is normalized per api —
pi's anthropic client appends /v1/messages itself, its openai clients want /v1 present.
auth.json and models.json are checkpoint-excluded: both can carry the literal key.

Pi ships without MCP by design, so MCP arrives as pi's own kind of answer: an extension.
pi-mcp-adapter (MIT, github.com/nicobailon/pi-mcp-adapter) installs next to the CLI at
first run, and the runner mounts it with -e only on turns that configure MCP servers,
writing mcp.json at the agent-dir level — the one location outside pi's project-trust
gate. Discovery stays off (--no-extensions): a task could otherwise drop .pi/extensions/
into the workspace and have the next turn execute it.

Disabled tools are enforcement on pi, not instruction: -xt is a real per-tool switch, so
the catalog entry says "hard" and means it. Skills land in the user-global skills dir
(project-local files sit behind the trust gate); AGENTS.md is a context file and loads
regardless.

The image moves node 20 -> 22: pi's engine floor is 22.19, node 20 has been EOL since
April, and claude/codex run unchanged. The release smoke test now asserts all four
backends install on a fresh volume, on both architectures.

Gateway: wiring rows for the seven integrations pi can serve, a multi-family model
catalog (gpt + claude families to start), base catalog entry, and pi in every
backend-name set. UI: the "coming soon" card becomes real — logo was already shipped.

15 new runner tests on the real captured stream; 337 gateway + 38 runner + tsc all green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GknTsrWVoAbJe8qX7i8QA3
The fresh-volume run caught it: 'installing Pi' printed, /data/agent-tools/bin/pi existed
and answered --version, and the summary still read 'backends available: claude codex
hermes' — the probe loop iterated a hardcoded three-name list. The install and the probe
now walk the same set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GknTsrWVoAbJe8qX7i8QA3
…e them first

The console's Keys page mints sk-hr-… keys "to make one and call the API", the /api/harness
proxy forwards Authorization through, and the gateway resolves the org from the key. None of
that could ever run on self-host: the middleware checked only the session cookie and answered
401 before the proxy did, so every bearer-only call died at the door. Measured before the fix:
cookie 200, cookie+garbage-bearer 200, valid-key-no-cookie 401 — the key was the one credential
that did not work. Found by the UHP conformance suite, which authenticates exactly this way and
skipped or failed 50 of 52 checks on HTTP 401.

Requests carrying a bearer in the key's own format now pass the gate on the API proxy path
only; the gateway stays the authority and 401s bad keys itself. Pages and every other route
remain cookie-gated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GknTsrWVoAbJe8qX7i8QA3
…eeds no key at all

Follow-through on the API-key fix, driven by the UHP suite's remaining failures. Letting
bearer calls past the cookie gate exposed the next assumption down: the BFF proxy stamped
internal trust and the pinned local identity on EVERY self-host request, because when it was
written the cookie gate in front of it was the only door. Measured: a syntactically valid but
nonexistent key read /v1/harnesses as the local org. Trust now follows the SESSION — a caller
with a valid cookie (the signed-in console) gets the internal path exactly as before; anyone
else forwards their Authorization bare and the gateway's _apikey_resolve decides, with its
revocation check and its canonical authentication_error body.

The gate also passes ANY bearer through, not just well-formed keys: an invalid credential must
earn the gateway's own 401 (error.type, UHP-Version header — the shape check A-02 requires),
not this gate's sign-in JSON. And /v1/uhp joins the public list, because discovery being
unauthenticated is itself a spec requirement (D-02): a client must be able to learn whether
this is a UHP server before deciding what credential to present.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GknTsrWVoAbJe8qX7i8QA3
…Version

The gateway stamps UHP-Version on every response — the protocol requires exactly that (V-01) —
and the BFF forwarded only its content/caching allow-list, so no client behind the console port
could ever see which contract it got. Forwarded now on both branches, SSE included.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GknTsrWVoAbJe8qX7i8QA3
…ENTS.md label

Three things the e2e run surfaced. The Add Harness dialog described Pi with Claude Code's
copy — literally the same sentence, which even broke test automation that tried to select a
card by its description. The base filter dropdown stopped at three backends. And a pi
harness's instructions field was labelled CLAUDE.md when the runner seeds AGENTS.md for pi.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GknTsrWVoAbJe8qX7i8QA3
…re it is listed

"Why does pi only support gpt and claude?" — it never did; the CLI runs anything
OpenAI/Anthropic-compatible. The catalog was narrow because the catalog is a verification
record, not a capability statement, and only two families had been through a checked turn.

So the other nine earned their rows the same way hermes's did: probed through the pi CLI on
the TokenRouter connection (openai-completions custom provider), every reply echoed exactly,
every response reporting the requested model id — the substitution check that exists because
nine models once "passed" a test that was claude answering nine times.

  gemini-3.6-flash, deepseek-v4-pro, deepseek-v4-flash, kimi-k3, kimi-k2.7-code,
  qwen3.7-max, qwen3.8-max, mistral-medium-3.5, step-3.7-flash

Five hermes models stay off pi's list, and the comment says exactly why: TokenRouter lists
no channel for them, hermes reaches them via OpenRouter, and no OpenRouter credential was
available to probe pi with. Unprobed is unlisted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GknTsrWVoAbJe8qX7i8QA3
…replay

The failing session was a genuinely multi-model conversation — claude-sonnet-5 with tools
(including a screenshot image in one tool result), then gpt-5.4-mini, deepseek, kimi — and
every follow-up worked until qwen3.7-max, which answered
  400 InvalidParameter: Unexpected item type in content.
A fresh session on the same model twice was fine, which is the tell: the poison was in the
HISTORY, not the model.

Isolated by direct probes on the TokenRouter connection: qwen3.7-max rejects image input
outright (a user message with an image part reproduces the exact 400); qwen3.8-max complains
about the 1x1 probe's DIMENSIONS (so it takes images), and gemini/deepseek/kimi/mistral/step
all accept. One text-only channel, one deny-list entry.

The mechanism was then verified against a capturing sink, not inferred: with the runner's
models.json declaring input ["text","image"] (what it always wrote), pi serializes a replayed
image tool-result as a separate user message carrying an image part — the rejected shape.
Declared ["text"], pi drops the image and the same resume completes. So pi's own capability
field is the fix: the gateway now says which models are text-only (measured, one entry), the
turn body carries it, and the runner writes `input` accordingly. A degraded answer that lost
one screenshot beats a conversation that can never continue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GknTsrWVoAbJe8qX7i8QA3
Seen live on a kimi channel: the streamed deltas and the message's final text disagreed, and
the self-healing re-emit painted the whole answer a second time in the transcript. The deltas
are already on screen, so the re-emit is suppressed for that one case; the no-deltas path still
emits the full text once, and the result event carries the authoritative final either way.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GknTsrWVoAbJe8qX7i8QA3
…rmes, Pi

The list sorted every row by last activity, so whichever base you had just tried jumped the
queue — Pi sat above Codex two minutes after its first task. Built-ins now keep a fixed
catalog order with Pi last; custom harnesses stay activity-sorted, above them, because the
things you made are what you manage on this page. Same order in the Add Harness dialog and
the gateway catalog.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GknTsrWVoAbJe8qX7i8QA3
Pi, the fourth harness — with MCP via the open-source adapter, e2e- and UHP-verified
…a Pi harness

Same suite, same instance class as the 0.3.0 report; this one exercised the task checks
through the new pi backend (gpt-5.4-mini on a TokenRouter integration), which is the release's
headline change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GknTsrWVoAbJe8qX7i8QA3
Named assuming the next version was 0.5.0; 0.5.x and 0.6.0 had already shipped from
alongside this branch. Same report, same run, right name.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GknTsrWVoAbJe8qX7i8QA3
… with a relay that unbreaks aggregator tool calls

dsh (deepseek-ai/deepseek-harness, MIT, developer preview) becomes a backend along the
lines its own audit drew (harnessrouter-management, 2026-08-13): the official Python SDK
(`deepseek-harness-sdk==0.1.0rc7`, pinned exactly, never `latest`) drives the bundled
JSON-RPC runtime executable (`deepseek-harness-runtime-bin`, same pin — no system Node
involved), and a per-turn driver re-emits every session.event as NDJSON for the runner's
normalizer. Cancel stays a process-group kill; the runtime dies with the group.

The Phase-0 spike the audit prescribed found the gap it predicted, just one layer lower:
dsh's LLM adapter accumulates streamed tool calls guarded by `!== undefined`, and
aggregators like TokenRouter send continuation deltas with EMPTY-STRING id/name where
OpenAI omits the fields — so the second delta clobbers the real name and every tool call
died as UNKNOWN_TOOL (captured live against deepseek/deepseek-v4-flash, raw SSE on file).
The audit's rule was to bridge outer-layer gaps without a fork, so the driver runs a
loopback relay that drops exactly those empty fields — and, as a consequence the credential
design gets for free, the REAL key lives only in the driver process: the runtime is
launched at the relay with a placeholder key, so the credential never enters the dsh
process env, its session log, or anything a checkpoint could carry.

Mapping follows the audit's table: reasoning/text deltas stream, committed
assistant/message emits only the un-streamed tail (the pi lesson), tool/call + tool/result
pair up by callId, usage is keyed by (turn, step) and REPLACED — the retry ladder
re-reports a step, and summing would double-bill — then summed at the end. turn/end
error/max-tokens/completed map to error / error_max_turns / success, and the terminal
event is the only status signal, because the driver exits 0 either way.

Single-family by design in this phase: the audit defers multi-provider composition until
one path proves model/usage/tool/error consistency, so the catalog is deepseek-v4-pro and
deepseek-v4-flash over the four OpenAI-compatible integrations. No first-party DeepSeek
Platform integration yet — nobody here holds a platform key, and unprobed is unlisted.
Instructions land in AGENTS.md (dsh reads it via dsh-agent-instructions); MCP servers
compose into the bundled cordis as dsh-mcp-client entries. Logo is upstream's own MIT
mark. 10 new runner tests on the captured fixtures; 337 gateway + 51 runner + tsc green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GknTsrWVoAbJe8qX7i8QA3
…d for the one gap it left shut

The deployed e2e hit it immediately: a follow-up turn died with `session "…" already has a
persisted log on disk that does not match this live session (id collision)`. Upstream's SDK
JSON-RPC server only ever calls ctx.agents.create(); the persistence coordinator's adoption
path needs an in-memory seed a fresh process cannot have, and the wire has no resume method.
Exactly the continuation gap the audit predicted — with its remedy: bridge it out-of-tree,
version-locked, without forking the runtime.

The bridge is hr_dsh_server.cjs, loaded as a CONFIGURATION-RELATIVE plugin (a documented
packaged-bin feature) from a cordis composition that is the wheel's own bundled default with
one entry swapped. It subclasses the exported HarnessSdkJsonRpcServer and overrides only
createSession: try ctx.agents.resume({resumeSessionId}) first, fall back to the parent's
create — so a fresh id costs one failed resume and a stored id loads its history. Everything
else (prompt handling, notifications, shutdown) stays inherited, verbatim upstream.

Two mechanical facts the file documents because they will bite whoever bumps the pin: the
plugin lives outside the runtime's pkg snapshot, so bundled modules resolve through a
createRequire anchored inside the snapshot's node_modules (derived from process.argv[1]);
and the override reaches TS-private fields, so it hard-refuses to boot against any bundled
version other than the pinned 0.1.0-rc.7.

Proven locally against the real runtime executable: three drives of one session id from
three separate processes — persisted zstd log on disk, zero collisions, every turn reaching
the LLM stage. The deployed instance reproduces the before; the e2e that follows proves the
after through the product path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GknTsrWVoAbJe8qX7i8QA3
… say so

The MCP e2e wedged a task at Working forever. Root cause in two parts: the pinned runtime
wheel (0.1.0rc7) does not bundle dsh's MCP client — upstream's sdk-runtime README documents
it as included, but the wheel predates that — and naming the entry fails the plugin tree,
which leaves the runtime process alive with a mute stdout, so the SDK's unbounded initialize
request waits forever. The driver now sets request_timeout_seconds=180 (a mute boot becomes
a loud RequestTimeoutError instead of a wedge) and, per the pi precedent, announces-and-skips
configured MCP servers on this runtime build: the turn still runs, the line says why the
tools are absent, and DSH_RUNTIME_HAS_MCP flips when the pin moves to a wheel that ships
the client.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GknTsrWVoAbJe8qX7i8QA3
…s a dsh plugin

The pleasing discovery first: dsh's multi-provider layer, dsh-llm-pi-ai, IS pi's unified
LLM library (@earendil-works/pi-ai) wrapped as a Cordis plugin — and probing the pinned
runtime wheel showed it bundled (unlike the MCP client, whose README-vs-wheel gap taught
that lesson). So the expansion is mostly routing the pi backend already proved: a
hand-declared "hr" route at the driver's loopback relay, api picked by model family
(claude -> anthropic-messages, gpt/codex -> openai-responses, else openai-completions),
model capability declared per turn — including input:["text"] for the one channel measured
to refuse images (the qwen lesson travels with the flag the gateway already sends).

Family decides the route, not the integration's name: deepseek models keep the verified
dsh-llm-deepseek launch path whichever endpoint serves them. The relay grows two duties it
did not have: forward protocol headers verbatim (anthropic-version), inject the credential
in BOTH spellings (authorization + x-api-key), and rewrite the empty-string tool-call
quirk only on chat/completions — the other protocols pass through byte-faithful. The real
key still never enters the runtime: the pi-ai route's apiKeyEnv resolves a placeholder.

Wiring mirrors pi's rows; the catalog carries the family set pending the e2e probes that
follow on this branch — any row the probes refuse comes back out before merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GknTsrWVoAbJe8qX7i8QA3
…client appends it

Same lesson the pi backend already paid for, re-learned at the relay: pi-ai's
anthropic-messages client appends /v1/messages to its base, so composing the hr route with
…:port/v1 produced /v1/v1/messages and every claude-family turn died invalid_request_error.
Bare base for anthropic-messages, /v1 for the openai apis, with a composition test pinning
both spellings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GknTsrWVoAbJe8qX7i8QA3
DeepSeek Harness, the fifth backend — official SDK transport, aggregator tool calls unbroken
…epSeek Harness harness

Task checks ran on claude-haiku-4.5 via the dsh backend's pi-ai route — the release's
headline change exercised inside the suite itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GknTsrWVoAbJe8qX7i8QA3
Retaken from a pristine 0.8.0 container so the frame matches what a new user actually
sees on first sign-in: Codex, Claude Code, Hermes, Pi, DeepSeek Harness. Alt text was
still counting three.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GknTsrWVoAbJe8qX7i8QA3
…he old one

Safari on a first visit was still served the three-harness image from an edge cache —
same URL, stale bytes, beyond any client's control. A new filename is the one cache-bust
that works everywhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GknTsrWVoAbJe8qX7i8QA3
Main grew its own CONTRIBUTING.md (and protocol/GOVERNANCE.md with the UEP
process) after this draft was written, so the add/add conflict is resolved
on main's version, with what this PR adds beyond it folded in: the
two-layer UHP/implementation framing, a pull-request checklist, and the
development-check commands. The README hunk is dropped — main's README
already links CONTRIBUTING.md from its community section, so the added
section would say the same thing twice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ibuting-governance

docs: add UHP contribution and governance guide
…', and the report dates itself

The README states the rule and the human renderer honours it, but to_json() set
conformant: true whenever nothing failed or errored — a run in which most checks
never executed serialised green, in the artifact GOVERNANCE.md expects to be
published as evidence, read by people who did not run it. Now:

- conformant is strict: every check ran and none failed or errored. One skip
  makes it false.
- conformant_with_skips keeps the old meaning under an honest name, and
  skipped_not_verified enumerates the check ids the report establishes nothing
  about.
- suite_version and generated_at (UTC) tie the report to the suite revision and
  the moment that produced it; the 0.3.0 report had to be dated in prose because
  the file could not date itself. Reports without these fields predate suite
  2026.8.11.post1.
- highest_class() goes strict the same way: fails, errors, skips, and classes
  with no results at all break the ladder — before, a run that only exercised
  core reported highest_class_passed: "full", crediting classes that never ran.
- The human summary speaks the same vocabulary: CONFORMANT WITH SKIPS, in
  yellow, instead of a green CONFORMANT with a footnote.

The checked-in reports are artifacts of the older suite and are left as they
are; the README says how to read their shape.

Closes HarnessRouter#7. Reported by @asj305, with the fix shape as proposed there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
release.yml gates what ships, but gateway/tests, runner/tests,
protocol/conformance/tests and the console's checks only ran when someone
remembered to. One job per suite so a failure names the area that broke;
ffmpeg is installed for the gateway job because the media tests gate
themselves on have_ffmpeg() and silently skip without it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jest exits 1 on 'No tests found' and the ui has no tests yet; the flag makes
an empty suite pass while a failing test still fails.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rip it for the turn

The deepseek-official adapter sends reasoning_effort on every request. DeepSeek's
own API takes it; aggregators serving deepseek/* over the OpenAI shape refuse the
whole request (LLMTR: 'The "reasoning_effort" parameter is not supported by
"deepseek/deepseek-v4-pro"', captured 2026-08-20 by conformance T-01 — four
full-class checks failed on that one rejection). The relay now retries such a
rejection once without the parameter and strips it for the rest of the turn, so
a provider that accepts it keeps it, and one that refuses it costs one extra
round trip, once.

Verified live: dsh on deepseek-v4-pro through LLMTR went 47/52 (4 failed,
1 skipped) to 52/52 full-class conformant, including X-07 — the artifact the
failed turn never produced now downloads.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… Error shape

The spec's Error object requires `type` from the closed enum, with `code` left
for the specific condition. The gateway emitted {"code": "harness_error",
"message": ...} — the type in the code slot and no type at all — so EVERY failed
turn produced a spec-invalid Response. Nothing noticed until a conformance run
actually failed a turn (TokenRouter rejecting hermes's empty text block, LLMTR
rejecting dsh's reasoning_effort, both 2026-08-20): T-01/S-03/S-07/X-05 then
flagged the envelope on top of the failure.

Now: harness-reported failures carry type=harness_error code=turn_failed, an
exhausted connection ladder carries type=harness_error code=connections_exhausted
with the tried-ledger in message, and an unhandled exception is honestly
type=server_error code=unhandled_exception. The SSE error event mirrors
error.code so the two never disagree.

gateway/tests/test_response_error_envelope.py validates fail()'s envelope and
the full failed Response against the schema, and asserts at the source that
every .error literal leads with a spec type.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
richard-epsilla and others added 25 commits August 25, 2026 13:43
The console showed opencode.json as a produced file on every turn. Produced files are
`git status` of the session workspace, and the config was written at the workspace root —
so internal configuration (the relay URL, the key env var name, data-volume paths) was
collected and presented to the user as if the agent had produced it. That is exactly the
internal-architecture leak the product rules forbid, and the same reason every other
backend keeps its config under .harness/.

Moved to .harness/opencode.json, with OPENCODE_CONFIG pointing the binary at it —
verified against the shipped 1.18.23: the config at that path is loaded and reaches the
provider. Regression test pins both the location and the env var.

137 tests pass, pyflakes clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7VoW2QzfDkxEoudktbjVY
The square mark from opencode.ai (its 180x180 apple-touch icon), replacing the generic
glyph fallback. Same mechanism as the other five: one file in /public/logos plus one row
in HarnessLogo's map.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7VoW2QzfDkxEoudktbjVY
…x hours

Seen live: the container was SIGKILLed mid-turn, and after restart the task sat at
'Working' with a finished-looking transcript. The reconcile sweep found the stale
heartbeat but could not settle it: the trace had no terminal status, adoption is gated on
the control store (absent self-hosted), so the only exit was the six-hour _GW_MAX_TURN_S
cap. The record was honestly reporting a process that no longer existed, forever-ish.

The sweep now asks the RUNNER whether the recorded turn still exists before considering
adoption. Turn records are in-memory there, so a restart answers 404 — and 404 means the
owning process is gone and the turn can never finish. That settles it as failed on the
next sweep cycle. An unreachable runner is NOT treated as gone: transient network means
retry later, never fail.

SaaS behaviour is unchanged: a live sandbox answers 200 and the adoption path proceeds
exactly as before; a reaped per-session sandbox now also settles promptly, which is
strictly better than the cap.

Verified by reproducing the incident end to end on the deployed container: started a
40-second turn, docker kill mid-flight, restart — the session settled to 'failed' within
one sweep cycle instead of showing 'Working' for six hours.

This is the recovery-pass-on-startup argument from issue HarnessRouter#25 landing in practice: if turn
ownership lives in a process, repairing state after a crash is part of the feature.

pyflakes clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7VoW2QzfDkxEoudktbjVY
…invent 'completed'

Follow-through on the restart-orphan fix, from the same live incident. Settling only the
session vertex left the turn's RESPONSE record at 'running', so the conversation view kept
a live 'Working…' spinner on a turn whose process was gone — the sweep removed the lie
from one record and left it in the other. The sweep now settles the session's
running_response_id in the same pass through _reconcile_response.

And a response settled from session-terminal state with NO output of its own now lands as
'incomplete', not 'completed'. Mapping the session's 'done' straight onto the response
invented a success: a 'continue finish the work' that never produced a byte read as
completed in the turn list. 'completed' is a claim about this response, not the session.

pyflakes clean. Verified on the live instance: the stuck thread settled to four terminal
turns with no spinner.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7VoW2QzfDkxEoudktbjVY
…imply collected

Watched the same deck get stranded twice in one evening, two different ways (an OOM kill
at finalize, then a cancel landing after the file was written). Root cause is structural:
/produced was uncommitted-vs-HEAD while /checkpoint moves HEAD unconditionally, so HEAD
doubled as the collection cursor. Any terminal path that skipped collection followed by
any checkpoint buried the turn's files: on disk, invisible to /produced, forever.

The cursor is now its own ref (refs/hr/collected):

- /produced lists everything since the last ACKNOWLEDGED collection — committed changes
  past the ref plus untracked files — so checkpoints in between change nothing
- POST /produced/ack commits and advances the ref; the gateway calls it ONLY after the
  files are captured to blobs, which is the one thing that makes collected mean collected
- the ref initializes to HEAD on first touch (hydrate semantics: what arrived in the
  checkpoint was not produced by any turn here), and .git is already inside the
  checkpoint tarball, so the cursor survives sandbox recycling
- the gateway now collects on EVERY terminal outcome — a cancelled or failed turn may
  already have written the deliverable, and skipping collection threw it away from the
  record while it sat in the workspace

A dead turn now strands nothing: whatever it left behind is simply produced by the next
turn that completes collection.

4 new tests pin the semantics; 141 pass; pyflakes clean both trees.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7VoW2QzfDkxEoudktbjVY
…ld automation

Spec is now served per version at /spec/<date>/<chapter> (spec-first, mirroring
MCP's /specification/<date>/…), with a header version dropdown and a /spec →
latest redirect. Only the specification is versioned; conformance, governance,
versioning and changelog stay unversioned. VERSIONS is the single source; build
asserts every listed version has a source tree and was built (no dropdown 404s).

Homepage reads title → definition → quick-links (neutral-standard order) and
carries the same on-this-page rail as every other page. The rail gains a
scroll-spy (32%-anchor / last-passed-heading, aria-current) and a flatter design.
Implementations page renamed Examples, grouped Example servers / Example clients.
README drops the named-implementations list and the commercial-product framing;
it points at the examples page (SSOT) and states no hosted service is required.

Automation: one host config at protocol/vercel.json (framework:null, build via
site/build.sh, output site/dist) that build.py also copies into dist, so the
Vercel Git build and a manual deploy share one source. build.sh is the single
build-step source Vercel and CI both call. requirements.txt pins Markdown; a
tests.yml `site` job runs the same build.sh via .python-version and asserts the
config stays wired. build.py self-checks internal links, #anchors, versions and
the description manifest, so a broken build fails the PR, not production.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ations-and-search

site: version-addressable spec URLs, TOC scroll-spy, and repo→web build automation
ACP-per-turn integration design for adding Devin as a runner backend
in HarnessRouter Community Edition. OpenCode is already supported;
this spec focuses on the proprietary Devin CLI via the Agent Client
Protocol.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- Model the ACP driver on _run_codex_appserver_bg (JSON-RPC over stdio).
- Add a pattern-alignment section comparing Devin to Codex app-server,
  Hermes, OpenCode, and Claude Code.
- Clarify custom dispatch in turn(), resume guard, provider catalog/wiring,
  and tool_enforcement.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Switch the Docker install section to the official curl-bash installer,
with HOME/XDG_DATA_HOME redirected to the data volume and a post-install
symlink into the existing $TOOLS/bin path.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Seven tasks covering runner normalizer, ACP driver, registry/dispatch,
gateway catalog/provider wiring, UI harness, Docker install, and
conformance. Modeled on the existing codex app-server driver and aligned
with the approved design spec.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ller

- Add 'devin' to the OobHarness backend union and list
- Add a devin logo mapping and a simple SVG mark
- Add devin to HR_BACKENDS and backend_bin in the entrypoint
- Add install_devin using the official curl-bash installer
- Redirect HOME/XDG_DATA_HOME to the data volume and link devin into $TOOLS/bin
- Add `ignoreDeprecations: "6.0"` to `ui/tsconfig.json` to silence the
  `moduleResolution=node10` and `baseUrl` deprecation warnings emitted by
  TypeScript 6.x in this workspace.
- Replace the placeholder Devin logo SVG with the official mark.
Devin's ACP server requires an authenticate JSON-RPC call with
the advertised auth method and the API key in _meta.api_key before
session/new or session/resume. The driver was skipping this step,
so session creation failed with "ACP host has not authenticated".

- Send authenticate after initialize/initialized.
- Wait for auth success before creating or resuming a session.
- Update the fake ACP server to advertise authMethods and accept
  authenticate.

Generated with Devin (https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…o DEVIN_API_KEY

The user will configure the API token as `DEVIN_API_KEY` in their
environment, so the runner now uses that name consistently:

- `turn()` sets `env["DEVIN_API_KEY"]` from `auth.api_key`.
- `_run_devin_acp_bg` reads `DEVIN_API_KEY` for the ACP `authenticate`
  `_meta.api_key` field.
- The fail-closed missing-credential message now references
  `DEVIN_API_KEY`.
- Update the design spec and implementation plan to match.

Generated with Devin (https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- ui/tsconfig.json: migrate from the deprecated `moduleResolution: "node"`
  and `baseUrl`/`ignoreDeprecations` combo to `moduleResolution: "bundler"`,
  so the Next.js production build passes under the project's TypeScript 5.x.
- docker/entrypoint.sh: the Devin install script runs an interactive
  `devin setup` at the end that fails in a headless container. Ignore the
  installer exit code and verify the binary exists before declaring the
  install successful, so the runner can use the user-supplied `DEVIN_API_KEY`
  at turn time.

This lets `docker build` succeed and `docker run -e HR_BACKENDS=devin
-e DEVIN_API_KEY=...` install and run Devin on the data volume, like the
other backends.

Generated with Devin (https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Replace the static six-model Devin catalog with live discovery against
the Codeium Connect-RPC GetCliModelConfigs endpoint. The gateway now
fetches a fresh model list per configured Devin API key, caches it for
five minutes, and exposes the latest SWE-1.x family id (currently
swe-1-7) plus the legacy devin-swe alias.

Discovery is fallible: missing credentials or network failures fall back
to the existing static catalog. The model labels from Devin are surfaced
in /v1/models and /v1/bases, and the vendor table is updated so the
runner receives an authorized model.

Local verification:
  - GET /v1/models returns devin: {default: swe-1-7, models: [swe-1-7, devin-swe]}
  - Turn with swe-1-7 and devin-swe both complete and write workspace files.
  - gateway/tests: 368 passed, runner/tests: 145 passed.

Generated with Devin

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@vercel

vercel Bot commented Aug 26, 2026

Copy link
Copy Markdown

@chenillen is attempting to deploy a commit to the Future HR Team on Vercel.

A member of the Team first needs to authorize it.

@kuanzema

Copy link
Copy Markdown
Contributor

Hi @chenillen, thank you for contributing this PR. We sincerely appreciate the time and effort you put into it.

We reviewed Cognition’s published terms and could not find language clearly authorizing a third-party Apache-2.0 project to integrate Devin in this manner. The terms limit the use of Devin to internal business purposes and restrict making the service available to others or developing competing products without Cognition’s written approval.

Are you submitting this integration on Cognition’s behalf and authorized to do so? If so, please provide written confirmation from Cognition covering this open-source integration. Otherwise, we will need to substantially revise the PR or place it on hold until we receive that approval. We are taking this step out of respect for the intellectual property rights of Devin’s creators.

We would be happy to discuss this privately and work with you toward a compliant path forward. You can find us on Discord, or contact@harnessrouter.ai

Thank you again. We are grateful to have you contributing to HarnessRouter.

@chenillen

Copy link
Copy Markdown
Author

The terms limit the use of Devin to internal business purposes and restrict making the service available to others or developing competing products without Cognition’s written approval.

Thanks for your reply, I am actually not on the behalf of Cognition, just want to integrate to HarnessRouter. This project looks awesome.

@kuanzema

Copy link
Copy Markdown
Contributor

Thank you, @chenillen. We truly appreciate your contribution. If you find other agent harnesses that you believe UHP and HarnessRouter should support, and whose licensing permits integration, we would warmly welcome those contributions.

We value the work you have put into this PR, so we will leave it open while we reach out to Cognition and invite the community to help establish contact. We hope to obtain either written approval for this integration or clarification of licensing terms that would permit it.

Thank you again for helping expand the UHP and HarnessRouter ecosystem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants