Skip to content

fix(specialists): one claude CLI on PATH, and specialists run only as CLI subprocesses - #1786

Merged
xiaofei-zheng merged 4 commits into
mainfrom
feature/leixin/claude-cli-before-geak
Oct 9, 2026
Merged

xiaofei-zheng merged 4 commits into
mainfrom
feature/leixin/claude-cli-before-geak

Conversation

@zengleixin-amd

@zengleixin-amd zengleixin-amd commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor
  • Description: Two installers placed Claude Code and three resolvers chose between the copies in different orders. GEAK's pip bootstrap installs the CLI into ~/.local/bin, off PATH, and runs before the kernel installer, whose Forge resolver then counted that copy as installed and skipped its own npm install. GEAK_CLAUDE_BIN was written as the first of ~/.local/bin/claude, /usr/local/bin/claude, PATH; Forge resolves PATH first; fix(specialists): spawn the Claude CLI the installer recorded #1775 made Hyperloom's agents read GEAK_CLAUDE_BIN. TopCI containers have started without a claude on PATH since 2026-09-24 (CLI(s) not on PATH: ['node', 'claude', 'codex'] on every run), and with HYPERLOOM_CLAUDE_CODE_VERSION set, npm put the pinned copy in /usr/local while GEAK_CLAUDE_BIN still named GEAK's copy, so the pin reached nobody. fix(specialists): spawn the Claude CLI the installer recorded #1775 already stops the specialists falling back to the in-process backend on TopCI (the 2026-10-09 06:21 UTC DeepSeek-V4.1-Flash run dispatched them as subprocesses), so this PR removes the second copy and the silent fallback that remains when no CLI resolves at all.
    This PR makes the installer own the one claude CLI every consumer runs:
    • ensure_claude_cli (renamed from ensure_forge_claude_cli) runs before ensure_geak. It puts ~/.local/bin first on PATH, reuses the claude PATH then resolves, and otherwise installs with Claude Code's native installer (curl -fsSL https://claude.ai/install.sh | bash -s <target>) into ~/.local/bin — the same command GEAK's bootstrap runs, so no Node.js is needed. ~/.local/bin goes first on PATH even when it was already on PATH further back, so HYPERLOOM_CLAUDE_CODE_VERSION reinstalls that version and it then wins on PATH. The install fails when curl is missing or the installer fails, instead of warning and continuing into a tool-less run. The embedded Python decision, its retry loop and the validate_runtime call go away; ClaudeBackend.validate_runtime stays where Forge runs it.
    • GEAK's bootstrap still runs, after ensure_claude_cli: it finds that claude and only steps in when it is older than GEAK's minimum (2.1.177), installing into the same ~/.local/bin, which is first on PATH. GEAK keeps ownership of its own floor, and no second copy lands off PATH.
    • GEAK_CLAUDE_BIN is command -v claude, and the ~/.local/bin PATH entry is written into kernel-agent.env.sh, so GEAK, the specialists, Forge and every sourcing shell resolve the same file.
    • Specialist dispatch raises at startup when neither HYPERLOOM_CLAUDE_CLI_PATH, GEAK_CLAUDE_BIN nor PATH yields a claude CLI, as a missing codex runtime already does, instead of falling back to the in-process backend, which has no source roots, permission mode or PR Monitor tools.
      With that fallback gone, the in-process specialist dispatch had no caller but an explicit --specialist-dispatch-mode inprocess, which no launcher passes (no TopCI job log since 2026-09-01 and no script on the shared NFS sets it). It is removed, so every specialist runs as a CLI subprocess in its own worktree:
    • --specialist-dispatch-mode and --specialist-per-turn-max-seconds (which only bounded in-process calls) are gone, with the in-process branch of the specialist executor and CodexAgentBackend, its only consumer. ClaudeBackend's attribution_component / attribution_operation fields, which only that branch overrode, give way to the orchestration labels they otherwise always carried.
    • SpecialistRunner takes a subprocess config and nothing else. Its backend_factory turn loop, which only tests drove, goes with the helpers only it used (partial-done writer, per-turn failure trace, partial path).
    • The prompt's emit_intent exit, which only the in-process loop honoured, is dropped: every specialist is told to write specialist_done.json.
    • A missing specialist_done is reported as no_specialist_done_emitted; max_turns_exhausted compared in-process turns and misread the subprocess's single turn whenever a task asked for one turn. Both classify as the same no-output failure.
  • Linked issue(s): none. Builds on fix(specialists): spawn the Claude CLI the installer recorded #1775 (resolve_claude_executable).
  • Tests: test_env_file_points_geak_and_later_shells_at_the_claude_on_path now puts a second copy in ~/.local/bin beside the claude on PATH; against origin/main's installer it fails (GEAK_CLAUDE_BIN names the ~/.local copy) and it passes here. test_install_home_paths.py drives the real ensure_claude_cli against a sandboxed PATH, HOME and a stand-in for the native installer: fresh install into ~/.local/bin on PATH, reuse of a copy already there, reuse of a claude on PATH, the version pin installing ahead of the image's claude, failure without curl or when the installer fails, --check-only, and credentials under $HOME. The Forge-validation cases go with the mechanism they described; the contract they shared, that the CLI and credentials are ensured whatever kernel backend is selected, holds by construction since ensure_claude_cli reads no backend setting and main() calls it unconditionally. test_install_geak_pip.py asserts that ensure_claude_cli runs before ensure_geak. test_version_pin_wins_when_the_home_bin_dir_is_already_on_path_behind_the_image_claude covers a ~/.local/bin already on PATH behind another claude: before the fix the pinned copy was installed but PATH and the env file still resolved the image's claude. test_cli_executors_wiring.py asserts the startup refusal and keeps fix(specialists): spawn the Claude CLI the installer recorded #1775's recorded-CLI case. The executor, lifecycle and runner tests that drove a mocked in-process backend now run the real subprocess path against a fake claude CLI (moved from test_specialist_subprocess.py into the package conftest.py, plus a no_done behaviour), so they cover the path that ships: adapter round trip, crash → stale, clean exit without a done → no_specialist_done_emitted, patch vetting off the event loop, dispatcher bookkeeping. Tests of the removed mechanisms are deleted: the in-process Codex/Claude backend selection, the runner's dispatch-exclusivity check, the partial-done writer, and the attribution-field guard (the call-site guard now sees ClaudeBackend's literal labels). On Linux, 235 test files covering the kernel installer, the slurm template, the CLI parser and executors, the specialist runner, dispatcher and prompts, and the Codex/Claude roles: origin/main 8749 passed / 12 failed / 1 error, this branch 8722 passed / 12 failed / 1 error, with identical failure sets (TraceLens, critic-backend and loop-runner environment failures on the runner); the 27 fewer passes are the removed cases above less the one added. ruff check and ruff format --check clean.
  • Size/complexity triggers crossed: none; 22 files, +639 / -1722. Four commits: the installer change; the dispatcher refusal; the in-process dispatch removal; the runner's subprocess-only path. They are kept in one PR because each removes a step of the same fallback chain: an install that left no claude, a dispatcher that degraded, and an in-process path that degraded runs landed on.
  • If this simplifies or refactors: removed mechanisms are the second Claude Code install off PATH, the installer's Forge-resolver decision with its validation loop, the NodeSource/npm path for claude, the in-process specialist dispatch (CLI flags, executor branch, CodexAgentBackend, the runner's turn loop and its emit_intent exit), and ClaudeBackend's attribution override fields. Preserved contracts: the version pin still reinstalls, credentials still land in $HOME/.claude/config.json, and --check-only still only warns.
  • Observable effect: after install, claude is on PATH (~/.local/bin/claude unless the image already ships one), so CLI(s) not on PATH no longer lists claude and GEAK_CLAUDE_BIN in kernel-agent.env.sh names the same file; the install log shows installing the claude CLI (latest) into …/.local/bin on images without one, where GEAK's bootstrap used to install its own copy off PATH. HYPERLOOM_CLAUDE_CODE_VERSION now takes the native installer's targets (stable, latest or an exact X.Y.Z) rather than an npm version spec. An install that cannot provide claude exits with an error instead of a warning, and a session whose specialists find no claude CLI stops at startup instead of running every specialist without tools. --specialist-dispatch-mode and --specialist-per-turn-max-seconds are no longer accepted. The specialist prompt's output section names only the specialist_done.json exit, and a specialist that ends without one is recorded with reason no_specialist_done_emitted. LLM spend labels are unchanged: orchestration turns stay orchestration/orchestrate_turn and specialists stay specialist. The installer no longer installs Node.js: the codex carrier in _incontainer.sh needs the image to provide node/npm. Since 2026-09-24 it already did not get them on images where GEAK had installed claude; it lost them only where GEAK's install also failed.
  • Breaking changes: yes. Command lines passing --specialist-dispatch-mode or --specialist-per-turn-max-seconds now fail argument parsing (no TopCI launcher passes them). Hosts that cannot reach claude.ai / downloads.claude.ai or lack curl now fail the install instead of continuing without a claude CLI (those runs had no working specialists), and an HYPERLOOM_CLAUDE_CODE_VERSION written as an npm range is rejected by the native installer.
  • PR addresses single concern: yes, how a specialist gets its claude CLI: one copy on PATH, a stop at install and startup when there is none, and no in-process path to fall back to.
  • Root cause is upstream (Magpie/TraceLens/GEAK/IntelliKit/AgentKernelArena), ticket filed: no. GEAK's bootstrap behaves as documented for standalone installs; the defect was Hyperloom letting it run first and then accepting its off-PATH copy.

🤖 Generated with Claude Code

@zengleixin-amd
zengleixin-amd force-pushed the feature/leixin/claude-cli-before-geak branch 2 times, most recently from a1ae6be to 317c8db Compare October 9, 2026 09:10
@zengleixin-amd zengleixin-amd changed the title fix(install): put the claude CLI on PATH before GEAK installs fix(install): own one claude CLI on PATH, installed before GEAK Oct 9, 2026
@zengleixin-amd zengleixin-amd changed the title fix(install): own one claude CLI on PATH, installed before GEAK fix(specialists): one claude CLI on PATH, and specialists run only as CLI subprocesses Oct 9, 2026
@zengleixin-amd
zengleixin-amd force-pushed the feature/leixin/claude-cli-before-geak branch from 842fa75 to 09e73ee Compare October 9, 2026 10:27
@zengleixin-amd
zengleixin-amd marked this pull request as ready for review October 9, 2026 10:30
@zengleixin-amd
zengleixin-amd requested a review from a team as a code owner October 9, 2026 10:30
@xiaofei-zheng

Copy link
Copy Markdown
Collaborator

PR #1786 -- fix(specialists): one claude CLI on PATH, and specialists run only as CLI subprocesses

What it does: GEAK's pip bootstrap installed Claude Code into ~/.local/bin off PATH and ran before the kernel installer, whose Forge resolver then counted that copy as installed and skipped its own npm install, so HYPERLOOM_CLAUDE_CODE_VERSION, GEAK_CLAUDE_BIN and PATH could each name a different file and TopCI ran with no claude on PATH at all. install.sh now owns one copy: ensure_claude_cli runs before ensure_geak, puts ~/.local/bin first on PATH, installs through Claude Code's native installer, dies rather than continuing into a tool-less run, and writes both that PATH entry and GEAK_CLAUDE_BIN="$(command -v claude)" into kernel-agent.env.sh. With the install fixed, specialist dispatch raises at startup when no CLI resolves instead of degrading to the in-process backend, and the in-process path that fallback was the last caller of -- --specialist-dispatch-mode, --specialist-per-turn-max-seconds, the executor branch, CodexAgentBackend, the runner's turn loop and its emit_intent exit -- is deleted.

Blocking issues: none

Checked: install.sh ensure_claude_cli / write_env_file / main against base 1c14e79 (call order, _ANTHROPIC_KEY_VAL at 363-367 being top-level so the move is safe, set -euo pipefail at :14 carrying the curl | bash failure into die, _claude_bin_dir never read unset under set -u); _claude_path_dir writer at install.sh:1464 against its reader at install.sh:1304; cli/executors.py:_build_specialist_executor and its only production caller at cli/__init__.py:2444, gated on research_lane_capacity > 0; roles/claude.py:resolve_claude_executable resolution order (HYPERLOOM_CLAUDE_CLI_PATH, GEAK_CLAUDE_BIN, PATH), so a launcher that sources the env file resolves even without the PATH entry; specialists/runner.py:_run_via_subprocess (turns_used=1, so base's max_turns_exhausted really did misread a one-turn task) and _finalize's reason change against the classifier in test_specialist_auto_retry.py -- both strings map to SpecialistFailureType.NO_OUTPUT; the partial-checkpoint reader at subprocess_.py:891-892 against its writer, the specialist CLI, still instructed at specialist_prompt_builder.py:1438,2176; policy/gate.py:1571 still enforcing SPECIALIST_MAX_TURNS_HARD_CAP after its comment was rewritten; _incontainer.sh.in:50 running install.sh before the case, so the claude carrier gains a CLI and the codex carrier's node/npm note is the one that had to change. Head-tree greps confirm no reader left for specialist_dispatch_mode, specialist_per_turn_max_seconds, CodexAgentBackend, attribution_component, attribution_operation, backend_factory, exit_channel or ensure_forge_claude_cli, in code or in docs.

Ran: nothing locally -- SKIPPED: test execution. This reviewer is on Windows with no WSL and the suite imports fcntl (cli/kb.py:9), so the test axis rests on CI at head 09e73ee: 12/12 test shards, both coverage jobs, ruff, pylint, mypy, bandit, CodeQL, gitleaks, reuse and the docs build green. SKIPPED: independent reader -- no second reviewer checked the two candidates I dropped.

Base: 1c14e79 | Head: 09e73ee

LGTM

@xiaofei-zheng xiaofei-zheng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues. Review detail and the paths checked are in the comment above.

Zeng and others added 4 commits October 9, 2026 19:06
Two installers placed Claude Code and three resolvers picked between the
copies in different orders. GEAK's pip bootstrap installs it into
~/.local/bin, off PATH, and runs before the kernel installer, whose Forge
resolver then counted that copy as installed and skipped its own npm
install. TopCI containers have since started with no claude on PATH. With
HYPERLOOM_CLAUDE_CODE_VERSION set, npm put the pinned copy in /usr/local
while GEAK_CLAUDE_BIN still named GEAK's copy, so the pin reached nobody.

ensure_claude_cli now runs before ensure_geak. It puts ~/.local/bin first
on PATH, reuses the claude PATH then resolves, and otherwise installs with
Claude Code's native installer into ~/.local/bin, the same command GEAK's
bootstrap runs, so no Node.js is needed. A version pin reinstalls that
version, which then wins on PATH. The install fails when it cannot
provide claude instead of warning and continuing into a tool-less run.
GEAK's bootstrap then finds that claude and only steps in when it is older
than GEAK's minimum, installing into the same directory. GEAK_CLAUDE_BIN
names the claude on PATH, and the ~/.local/bin entry is always written
into kernel-agent.env.sh for every shell that sources it.

Co-Authored-By: Claude <noreply@anthropic.com>
…egrading

Subprocess dispatch that resolved no claude CLI logged a warning and fell
back to the in-process backend, which has no source roots, no permission
mode and no PR Monitor tools, so every specialist task came back empty
while the session reported success. When neither HYPERLOOM_CLAUDE_CLI_PATH,
GEAK_CLAUDE_BIN nor PATH yields one, startup now raises, as a missing codex
runtime already does.

Co-Authored-By: Claude <noreply@anthropic.com>
Specialists ran either as a CLI subprocess in a per-task worktree or, with
--specialist-dispatch-mode inprocess, through a Claude or Codex Agent SDK
backend inside the orchestrator, which gives them no source roots, no
permission mode and no PR Monitor tools. Production only ever used the
subprocess, and with the silent fallback gone nothing reached the
in-process path except an explicit flag no launcher passes.

The flag and --specialist-per-turn-max-seconds, which only bounded
in-process calls, are removed, along with the in-process branch of the
specialist executor and CodexAgentBackend, whose only caller it was.
ClaudeBackend's attribution_component and attribution_operation fields,
which only that branch overrode, give way to the orchestration labels
they otherwise always carried. The
executor tests that drove a mocked in-process backend now run the real
subprocess path against a fake claude CLI shared through conftest.

Co-Authored-By: Claude <noreply@anthropic.com>
With the in-process dispatch gone from the CLI, SpecialistRunner's
backend_factory path only served tests: it drove a mocked backend turn by
turn through a loop production never ran, so those tests did not cover the
subprocess path that ships. The runner now takes a subprocess config and
nothing else, and the turn loop goes with the helpers only it used: the
partial-done writer, the per-turn failure trace and the partial path.

The prompt's emit_intent exit, which only the in-process loop could
honour, is dropped; every specialist is told to write specialist_done.json.
A missing specialist_done is reported as no_specialist_done_emitted rather
than max_turns_exhausted, which counted in-process turns and misread the
subprocess's single turn. Runner tests that used a mocked backend now run
against the fake claude CLI.

Co-Authored-By: Claude <noreply@anthropic.com>
@zengleixin-amd
zengleixin-amd force-pushed the feature/leixin/claude-cli-before-geak branch from 09e73ee to 5f169ab Compare October 9, 2026 11:11

@xiaofei-zheng xiaofei-zheng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at head 5f169ab after the rebase onto 87bf19e. Conclusion unchanged: no blocking issues.

What moved: main relocated the installer (src/hyperloom/agents/kernel/scripts/install.sh -> src/hyperloom/inference_optimizer/assets/install_kernel_tools.sh) and its two test files into inference_optimizer/tests/. This PR's own 22-file change is byte-identical across the rebase -- I split both PR diffs per file, normalised the renamed paths and compared: same file set, same hunk bodies, with one difference inside a line this PR deletes (INSTALL_SH.parents[4] -> parents[3]), which is main's depth change to code the PR removes. Per-file line counts are unchanged (+639 / -1722).

Re-checked at the new head rather than assumed: ensure_claude_cli still sits immediately before ensure_geak in main() and _ANTHROPIC_KEY_VAL is still assigned at top level (install_kernel_tools.sh:364), so the credential write still sees it; set -euo pipefail is still line 14, so the curl | bash failure still reaches die; _claude_path_dir writer at :1457 against its reader at :1299. Both moved tests resolve the new installer path (test_install_geak_pip.py:15, test_install_home_paths.py:21), so test_claude_cli_is_on_path_before_geak_installs and the _extract_func-driven cases are reading the file they are meant to. The operator-facing message in cli/executors.py:118 was updated to the new path, and git grep finds no remaining reference to the old one anywhere in the tree. _incontainer.sh.in:142's "install.sh does not provide node/npm" still refers to assets/install.sh, the script the carrier actually runs at line 50, so it is still accurate.

CI at 5f169ab: ruff, pylint, mypy, bandit, gitleaks, reuse, byte-compile, wheel/pyproject and the docs build are green; the test shards and CodeQL were still running when I looked.

@xiaofei-zheng
xiaofei-zheng merged commit 3a07715 into main Oct 9, 2026
37 checks passed
@xiaofei-zheng
xiaofei-zheng deleted the feature/leixin/claude-cli-before-geak branch October 9, 2026 11:20
zengleixin-amd pushed a commit that referenced this pull request Oct 15, 2026
The section carried over the Unreleased entries verbatim, and those
missed changes that ship in 1.1.4 and that an operator upgrading from 1.1.3
will hit:

- the single `hyperloom <command>` entry point and the old entry points,
  flags and critic subcommands it removed (#1736);
- the removal of in-process specialist dispatch and its two flags, and the
  installer owning the one claude CLI (#1786);
- kernelforge no longer reading its historical workspace formats, and the
  `--op-name` alias and old entry-point group it dropped (#1768);
- four of the variables #1739 made fail on a malformed value were missing
  from the strict-parsing list: INFERENCE_OPTIMIZER_CYCLE_RELOOP_MIN_REMAINING_SEC,
  HYPERLOOM_GEMM_PAIRED_PAIRS, MAX_MODEL_LEN and GPU_MEMORY_UTILIZATION.

Co-Authored-By: Claude <noreply@anthropic.com>
lishuoshuo-amd pushed a commit that referenced this pull request Oct 15, 2026
* chore(release): cut 1.1.4

Every site that tracks the packaged version moves together: pyproject's
[project].version, the docs version_number kept in sync with it, the README
badge, the compatibility matrix row, and the three copy-paste
`pip install hyperloom-inference-optimizer==` pins, which would otherwise keep
installing 1.1.3 after 1.1.4 ships.

HYPERLOOM_WHEEL_TAG moves with it, which requires the v1.1.4 GitHub release to
carry the rocm-profiler-hotfix-libs.tar.gz asset the bare-metal installer
downloads from that tag.

The Unreleased section of the release notes becomes the 1.1.4 section, and an
empty Unreleased heading is kept for the next cycle.

Co-Authored-By: Claude <noreply@anthropic.com>

* docs(release-notes): complete the 1.1.4 section

The section carried over the Unreleased entries verbatim, and those
missed changes that ship in 1.1.4 and that an operator upgrading from 1.1.3
will hit:

- the single `hyperloom <command>` entry point and the old entry points,
  flags and critic subcommands it removed (#1736);
- the removal of in-process specialist dispatch and its two flags, and the
  installer owning the one claude CLI (#1786);
- kernelforge no longer reading its historical workspace formats, and the
  `--op-name` alias and old entry-point group it dropped (#1768);
- four of the variables #1739 made fail on a malformed value were missing
  from the strict-parsing list: INFERENCE_OPTIMIZER_CYCLE_RELOOP_MIN_REMAINING_SEC,
  HYPERLOOM_GEMM_PAIRED_PAIRS, MAX_MODEL_LEN and GPU_MEMORY_UTILIZATION.

Co-Authored-By: Claude <noreply@anthropic.com>

* docs(release-notes): cover every operator-visible change in 1.1.4

The 1.1.4 section listed six of the 98 operator-visible changes merged
between v1.1.3 and the release branch. Add a "Before upgrading from 1.1.3"
section for the removed options, module paths and environment variables,
the moved defaults, the installation prerequisites and the persisted-state
changes, and add Added, Changed, Removed and Fixed entries for the rest.
1.1.4 is no longer described as a patch release.

The fixes for the AgentX agentic-recipe path (#1696, #1749) are left out:
the path itself is removed in the same release (#1784).

Allow the release notes to name the retired kernel_agents.agent_providers
entry-point group and KERNEL_AGENTS_MAX_TURNS in the rename-completeness
test, as they already may name the retired kernel-agents console script.

Co-Authored-By: Claude <noreply@anthropic.com>

* docs(release-notes): correct six 1.1.4 entries against the code

- GLM-5.3 corpus: the old corpus stays usable through
  AGENTX_CANONICAL_DATASET; pinning it with AGENTX_DATASET stamps every
  result submission_valid=false.
- Workload knobs: state that --extra-env pins are now exported under their
  own names, that a shell PRECISION still overrides a resumed session, and
  that 1.1.3 already read MAX_MODEL_LEN.
- Resume still type-checks crash_count / crash_timestamps; drop that claim.
- optimization_journal.json outcomes: name the literals it writes
  (KEEP, REVERT, no_promote, recorded).
- Roofline: the profile runs once and logs one "roofline profile failed"
  warning.

Also quote the real UPDATE_STATE refusal, note that the MLPerf backend
measures Kimi-K3 only, scope the warm-replay flag removal to the flag, and
write placeholders as <name> like the rest of the page.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Zeng <lzeng@amd.com>
Co-authored-by: Claude <noreply@anthropic.com>
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.

2 participants