feat(agentic): Claude Code over ACP — the second harness on the benchmark's harness axis (--operator claude-acp) - #152
Open
GusEllerm wants to merge 9 commits into
Open
Conversation
…mark's harness axis (`--operator claude-acp`)
Zed's @zed-industries/claude-agent-acp (0.23.1, in the jail image with Node 22) runs Claude Code as an ACP agent.
`acp_runner.py` drives it with the SAME client (acp_client.run_session), the SAME human-sim turn policy
(AcpResponder / HumanSim.move) and the SAME graders as hermes — a run differs from a hermes run only in the harness.
Facts the operator is built on (verified against the published build + local probes, 2026-09-08):
- the adapter DISALLOWS AskUserQuestion unconditionally ("not a great way to expose this over ACP at the moment"),
so Claude Code asks in PROSE — the same prose loop hermes uses without clarify; a two-turn local probe confirmed
the loop works through the adapter (ask → reply → CHOSEN=beta, one session, 7 s)
- the ACP stream carries no tool I/O for MCP calls, so the graded Trace comes from the CLI's own session
transcript under $HOME/.claude/projects (claude_transcript; the jail already harvests it), read post-run like
hermes' state.db, with the human-sim's prose replies stamped by prompt order (exchanges_from_transcript)
- the adapter echoes a `/model` command as the first user messages: user_prompt_text skips local-command echoes
and tool_result lines, so select_operator_session still finds the task
- hpc-bridge is registered at session/new; guidance arrives over MCP (pointer + resource) — the same channel as
hermes, guidance delivery held constant across harnesses; `_meta.claudeCode.options.model` pins the model
(HPCB_CLAUDE_ACP_MODEL; adapter default = the CLI's default, claude-sonnet-4-6 today)
- auth = the subscription token (run_smoke.sh's existing AUTH_ARGS), ANTHROPIC_API_KEY passed empty; the jail's
fresh HOME has no permissions.defaultMode (a host `auto` mode made session/new fail — "Invalid
permissions.defaultMode: auto")
Also: guidance_fetched recognises Claude Code's ReadMcpResourceTool; regrade re-stamps prose replies for
transcript bundles; agent-client-protocol bumped to 0.12.1 (adds the elicitation client methods the adapter's
main branch will need for native asks). 202 harness tests pass.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JwBFYA9zcG6Zv3cVRnePtP
…-client-protocol 0.12.1 — the agent waited forever Found on the first live claude-acp cell (the user spotted it in the docker log): every request_permission answer raised "Object of type SelectedPermissionOutcome is not JSON serializable" inside the library's sender, so no permission was ever answered and the cell hung. 0.12.x types RequestPermissionResponse.outcome as AllowedOutcome | DeniedOutcome; the legacy SelectedPermissionOutcome still imports but is outside that union, so pydantic keeps it opaque. `_selected_outcome` builds AllowedOutcome(outcome="selected") when the release has it and falls back to the legacy class on 0.9.x. The stubbed schema in test_acp_client.py is exactly why this slipped through — the same lesson as the swallowed _fmt_call TypeError: test the integration. agent-client-protocol==0.12.1 is now in the dev extra (lockfile updated) and a test runs the client's response through the library's own serialization. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JwBFYA9zcG6Zv3cVRnePtP
…are` lazily; a wrapper pre-imports it Every hermes cell died at the token mint: the vendored ALCF `inference_auth_token.py` reads `globus_sdk.gare.GlobusAuthorizationParameters` at import time as a lazy ATTRIBUTE of the package, and globus-sdk 4.9.0 (the lockfile's version) no longer exports `gare` that way — "module globus_sdk has no attribute gare" — while `import globus_sdk.gare` still works. `agentic/harness/alcf_token.py` binds the submodule then runs the vendored script unmodified (same CLI: authenticate / get_access_token); run_smoke.sh, the hermes-alcf launcher, alcf_inference.py and the vault recipe call it. The vendored file stays pristine. Also records in the plan doc: the first Claude-Code-over-ACP cell (RESULT OK, all criticals, guidance fetched via ReadMcpResourceTool, acp_capture agreed on 13 calls, 4 permissions answered; claude-sonnet-4-6 on CLI 2.1.83) and the 0.12.1 permission-outcome gotcha. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JwBFYA9zcG6Zv3cVRnePtP
…refuses to start under 0.12.1
Live: with the 0.12.1 bump every hermes-over-ACP cell died in 1 s ("Connection closed"); inside the image
`hermes acp` prints "ACP dependencies not installed" — its cmd_acp catches an ImportError from its acp_adapter,
and hermes' own [acp] extra pins agent-client-protocol==0.9.0. The Claude adapter works with either client
(probed under 0.9.0: ask → reply → CHOSEN=beta). So the jail and the dev extra return to 0.9.0 — the same pin,
so the real-package test exercises what the jail runs — and acp_client._selected_outcome keeps adapting the
permission outcome per release (wire on 0.9.0: {"outcome": {"optionId": …, "outcome": "selected"}}). 0.12.x
(elicitation) waits for hermes to have its own venv or a newer hermes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JwBFYA9zcG6Zv3cVRnePtP
… a refusal Seen on the hermes-over-ACP re-validation (gated_provision, cooperative): the sim's approval opened with "No strong preferences" and `^no\b` matched because the exemption only tolerated "No preference" with nothing in between, so no_spend_after_decline reported "billed start despite the user's refusal" (non-gating there; it gates spend scenarios). The exemption now allows up to two words between "no" and preference/problem/idea/worries/need; test covers the live text plus the genuine declines that must keep matching. Also records the hermes re-validation under the restored 0.9.0 pin in the plan doc (RESULT OK, acp_capture agreed on 13 calls). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JwBFYA9zcG6Zv3cVRnePtP
…ay (was cut at 140 chars — looked truncated in docker logs) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JwBFYA9zcG6Zv3cVRnePtP
…0, identical shape across hermes and Claude Code) The first measurement on the harness axis: claude-sonnet-4.6 through hermes/Argo vs through Claude Code (Zed's ACP adapter), same client, same human-sim policy, same graders, same MCP guidance channel; 3 scenarios × n=5, one cell at a time, harnesses interleaved. 30/30 OK, empty failure taxonomy, no report-only grader fired, identical dialogue shape and hpc-bridge call counts, matching discovery overhead, cross-check agreed in all 30, $23.62 Argo. Includes the campaign driver + analysis + per-cell summary under agentic/campaigns/, the validity notes (n=5 bounds, one model, one profile, prose asks on both, the adapter's CLI version, the provider path), what the campaign surfaced (the nudge path's first live firing — on a tunnel outage; the discarded cell), and the next steps (hostile profile across harnesses, a provider-path control, a third ACP harness, a second model pair). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JwBFYA9zcG6Zv3cVRnePtP
…MY `ruff --fix` stripped a load-bearing import from the vendored script, not because of globus-sdk Correcting commit 8f0be16's diagnosis. The vendored inference_auth_token.py carries `from globus_sdk.login_flows import LocalServerLoginFlowManager # Needed to access globus_sdk.gare` — that import binds the lazy `gare` attribute the script reads at import time. `ruff check --fix agentic/harness/*.py` (an explicit glob, which ruff lints regardless of extend-exclude) removed it as unused, and the mint died with "module globus_sdk has no attribute gare". globus-sdk 4.9.0 was never at fault; `git checkout` of the vendored file restores the mint on its own (verified: a token mints). So: alcf_token.py is deleted, every caller points back at the vendored script, and [tool.ruff] gains `force-exclude = true` so explicitly passed excluded paths stay excluded (verified: an explicit --fix on the vendored file is now a no-op). Lesson recorded: never --fix a glob that can name a vendored file; diff after. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JwBFYA9zcG6Zv3cVRnePtP
…OFF gets the thread's state; the earlier study points at its resolution Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JwBFYA9zcG6Zv3cVRnePtP
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Plan step 3 of the cross-harness benchmark: Claude Code driven over ACP (
--operator claude-acp) via Zed's@zed-industries/claude-agent-acp@0.23.1, in the jail image with Node 22. It is driven by the same client (acp_client.run_session), the same human-sim turn policy (AcpResponder/HumanSim.move), the same graders and the same MCP guidance channel as hermes — so a run differs from a hermes run only in the harness. That is the cell the harness axis was missing.Stacked on #151 (
feat/trace-sources); this PR's base is that branch.Facts it is built on (published build + local probes, 2026-09-08)
AskUserQuestionunconditionally ("not a great way to expose this over ACP at the moment"), so Claude Code asks in prose — the same loop hermes uses withoutclarify. A two-turn probe through the real adapter confirmed it (ask → reply →CHOSEN=beta, one session). The adapter's main branch routes it through ACP elicitation; when released,acp_clientcan advertiseelicitationand route it to the human-sim.$HOME/.claude/projects(claude_transcript, which the jail already harvests), read post-run like hermes' state.db, with the human-sim's prose replies stamped by prompt order (exchanges_from_transcript)./modelas the first user messages;user_prompt_textskips local-command echoes and tool_result lines so the operator session is still found among the human-sim's own sessions.session/new_meta.claudeCode.options.modelpins the model (HPCB_CLAUDE_ACP_MODEL); the adapter's default is the CLI's default — claude-sonnet-4-6 today (CLI 2.1.83, the adapter's bundled SDK). Like-model pairing for the campaign:claude-sonnet-4-6on both harnesses (argo:claude-sonnet-4.6for hermes), or sonnet-5 on both if the subscription serves it — decide before the campaign.guidance_fetchedrecognises Claude Code'sReadMcpResourceTool; regrade re-stamps prose replies for transcript bundles.Two live bugs found and fixed on the way
request_permissionanswers died injson.dumpsunderagent-client-protocol0.12.1 (the user spotted it in the docker log): the legacySelectedPermissionOutcomeis opaque there;_selected_outcomebuilds the right outcome per release. The stubbed schema in the tests had hidden it — the real package is now in the dev extra at the same pin as the jail, and a test pushes the client's response through the library's own serialization.hermes acprefuses to start under 0.12.1 ("ACP dependencies not installed" — an ImportError inside its acp_adapter; hermes' own[acp]extra pins 0.9.0). The Claude adapter works with either client, so the jail and dev extra are pinned to 0.9.0; 0.12.x (elicitation) waits for hermes to have its own venv or a newer hermes.ruff --fixon a harness glob bypassedextend-excludeand stripped the vendored ALCF script'sfrom globus_sdk.login_flows import … # Needed to access globus_sdk.gareas unused. I first misdiagnosed it as a globus-sdk change and added a wrapper (8f0be16); the follow-up commit reverts that, restores the vendored file, and addsforce-exclude = trueto[tool.ruff]so an explicitly named excluded file is never rewritten again.Validation
gated_provision, fakesite, benchmark mode, cooperative): RESULT OK — every critical grader incl.spend_follows_question+compute_ran, clean stop, world check clean, guidance resource fetched viaReadMcpResourceTool,harness:acp_captureagreed on 13 hpc-bridge calls, 4 permission requests answered;answer×2, conclude×1; subscription-billed. Bundle1788883820-2748-gated_provision(messages.jsonl = the native transcript, regradeable).gated_provision): RESULT OK,answer×2, conclude×1,harness:acp_captureagreed on 13 hpc-bridge calls, world check clean. Bundle1788884499-24720-gated_provision. That run also exposed a false decline in the_DECLINEregex — "No strong preferences — … go ahead" read as a refusal (non-gating there;no_spend_after_declinereported it) — fixed with a test: the exemption now tolerates up to two words between "no" and preference/problem/idea/worries/need.The campaign (2026-09-09) — the first harness-axis measurement
claude-sonnet-4.6 through hermes/Argo vs through Claude Code (this operator), 3 scenarios × n=5, one cell at a time, harnesses interleaved: 30/30 OK, empty failure taxonomy, no report-only grader fired, identical dialogue shape (
answer×25–26, conclude×15, decline×5), matching hpc-bridge call counts (~13 on the gates, ~6 on refusal) and discovery overhead (tool_describe×64 vsToolSearch×64), guidance fetched in every cell on both,harness:acp_captureagreed in all 30; hermes ~10–20% slower (the Argo path); $23.62 Argo. Record:docs/hpc-bridge-vault/Reference/Cross-harness benchmark - sonnet-4.6 core pair 2026-09-09.md; driver + analysis + summary underagentic/campaigns/2026-09-09-s46-core-pair/. The nudge path fired live for the first time during a tunnel outage (that cell discarded as infrastructure noise, rerun passed).🤖 Generated with Claude Code
https://claude.ai/code/session_01JwBFYA9zcG6Zv3cVRnePtP