Skip to content

feat(lucebox): hub CLI + autotune/sweep/profile + harness adapters + shell wrapper#335

Open
easel wants to merge 13 commits into
Luce-Org:mainfrom
easel:feat/lucebox-cli
Open

feat(lucebox): hub CLI + autotune/sweep/profile + harness adapters + shell wrapper#335
easel wants to merge 13 commits into
Luce-Org:mainfrom
easel:feat/lucebox-cli

Conversation

@easel

@easel easel commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds the lucebox hub CLI (autotune, sweep, profile, smoke, models, config, download, host-check, docker_run) plus the lucebox.sh wrapper and install.sh bootstrap. Also lands the harness/ adapter package (claude_code, codex, hermes, openclaw, opencode, pi) and the run_lucebench.sh shim that lucebox bench-style flows shell out to. Additive only — no existing source files modified outside CI workflow edits, README touch-ups, and a lefthook.yml for the new packages.

Files

  • lucebox/ — new Python package: CLI entrypoint, autotune/sweep/profile/smoke/config/download/host-check/docker_run modules, full pytest suite under lucebox/tests/.
  • harness/ — new Python package: harness.bench + per-agent client adapters (claude_code.py, codex.py, hermes.py, openclaw.py, opencode.py, pi.py) and the run_lucebench.sh operator shim.
  • lucebox.sh + install.sh — top-level shell wrapper and bootstrap installer.
  • scripts/check_lucebox_wrapper_sandbox.sh, scripts/test_lucebox_sh.sh — wrapper-shape integration tests.
  • .github/workflows/ci.yml — wires the new packages into CI.
  • harness/clients/README.md, lefthook.yml — supporting docs/config.

Single commit, ~10.5k LOC added across 49 files.

Dependencies

Unit tests in lucebox/tests/ and harness/ mock the external surfaces and pass standalone, but end-to-end validation requires the three siblings above.

Test plan

Note: this PR's CLI/adapter code is self-contained and unit-tests pass standalone, but full integration validation requires the docker, luce-bench, and server-layer-split siblings to land together.

Generated with Claude Code

@easel easel force-pushed the feat/lucebox-cli branch 2 times, most recently from 91276a7 to 62b21f0 Compare June 4, 2026 02:50
easel added a commit to easel/lucebox-hub that referenced this pull request Jun 4, 2026
## What

Containerization stack for lucebox-hub. Dockerfile + docker-bake.hcl
build the lucebox-hub image (build-env and runtime stages);
scripts/build_image.sh drives local builds; server/scripts/entrypoint.sh
emits IMAGE_INFO / HOST_INFO sidecars consumed by /props. GitHub Actions
add .github/workflows/docker.yml (build & publish), update ci.yml, and
add release-luce-bench.yml for tagging. Workspace-root files
(pyproject.toml, uv.lock, Makefile, lefthook.yml, .gitignore, README)
live here because the Dockerfile uv-syncs the workspace at build time.

## Why

Provides the reproducible image and CI pipeline every other split PR
deploys into. Centralizing build/publish here keeps Dockerfile,
entrypoint, and workspace-root pinning in one reviewable change.

## Dependencies

- Luce-Org#335 (lucebox-cli): Dockerfile COPYs lucebox/ into the image
- Luce-Org#337 (lucebench-harness): Dockerfile COPYs luce-bench/ into the image
@easel easel force-pushed the feat/lucebox-cli branch from 62b21f0 to 8732b35 Compare June 4, 2026 05:02
easel added a commit to easel/lucebox-hub that referenced this pull request Jun 4, 2026
## What

Containerization stack for lucebox-hub. Dockerfile + docker-bake.hcl
build the lucebox-hub image (build-env and runtime stages);
scripts/build_image.sh drives local builds; server/scripts/entrypoint.sh
emits IMAGE_INFO / HOST_INFO sidecars consumed by /props. GitHub Actions
add .github/workflows/docker.yml (build & publish), update ci.yml, and
add release-luce-bench.yml for tagging. Workspace-root files
(pyproject.toml, uv.lock, Makefile, lefthook.yml, .gitignore, README)
live here because the Dockerfile uv-syncs the workspace at build time.

## Why

Provides the reproducible image and CI pipeline every other split PR
deploys into. Centralizing build/publish here keeps Dockerfile,
entrypoint, and workspace-root pinning in one reviewable change.

## Dependencies

- Luce-Org#335 (lucebox-cli): Dockerfile COPYs lucebox/ into the image
- Luce-Org#337 (lucebench-harness): Dockerfile COPYs luce-bench/ into the image
@easel easel force-pushed the feat/lucebox-cli branch 2 times, most recently from 6c9078b to 360d332 Compare June 4, 2026 17:17
easel added a commit to easel/lucebox-hub that referenced this pull request Jun 4, 2026
## What

Containerization stack for lucebox-hub. Dockerfile + docker-bake.hcl
build the lucebox-hub image (build-env and runtime stages);
scripts/build_image.sh drives local builds; server/scripts/entrypoint.sh
emits IMAGE_INFO / HOST_INFO sidecars consumed by /props. GitHub Actions
add .github/workflows/docker.yml (build & publish), update ci.yml, and
add release-luce-bench.yml for tagging. Workspace-root files
(pyproject.toml, uv.lock, Makefile, lefthook.yml, .gitignore, README)
live here because the Dockerfile uv-syncs the workspace at build time.

## Why

Provides the reproducible image and CI pipeline every other split PR
deploys into. Centralizing build/publish here keeps Dockerfile,
entrypoint, and workspace-root pinning in one reviewable change.

## Dependencies

- Luce-Org#335 (lucebox-cli): Dockerfile COPYs lucebox/ into the image
- Luce-Org#337 (lucebench-harness): Dockerfile COPYs luce-bench/ into the image
@easel easel force-pushed the feat/lucebox-cli branch 2 times, most recently from 1d588db to 0d8e1ff Compare June 4, 2026 18:38
@easel easel marked this pull request as ready for review June 4, 2026 19:18

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

29 issues found across 52 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lucebox/src/lucebox/docker_run.py">

<violation number="1" location="lucebox/src/lucebox/docker_run.py:231">
P3: Several newly added helper functions are dead code (defined but never called anywhere in the codebase).</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread harness/src/harness/clients/opencode.py Outdated
Comment thread lucebox/src/lucebox/autotune.py Outdated
Comment thread harness/src/harness/bench.py Outdated
Comment thread lucebox.sh Outdated
Comment thread lucebox/src/lucebox/sweep.py Outdated
Comment thread lucebox/src/lucebox/sweep.py Outdated
Comment thread harness/clients/run_lucebench.sh Outdated
Comment thread lucebox.sh Outdated
Comment thread lefthook.yml Outdated
Comment thread lucebox/src/lucebox/docker_run.py Outdated
# ── subprocess helpers ─────────────────────────────────────────────────────


def run(argv: list[str], *, check: bool = True) -> subprocess.CompletedProcess[str]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: Several newly added helper functions are dead code (defined but never called anywhere in the codebase).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lucebox/src/lucebox/docker_run.py, line 231:

<comment>Several newly added helper functions are dead code (defined but never called anywhere in the codebase).</comment>

<file context>
@@ -0,0 +1,270 @@
+# ── subprocess helpers ─────────────────────────────────────────────────────
+
+
+def run(argv: list[str], *, check: bool = True) -> subprocess.CompletedProcess[str]:
+    """Run a command, streaming stdout/stderr to the user. `check=False` to
+    inspect exit codes manually."""
</file context>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 73f04f4: removed the unused run(), docker_inspect_running(), host_path_visible(), and stderr() helpers from docker_run.py, plus the now-unused sys import.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the update!

@easel easel force-pushed the feat/lucebox-cli branch from 0d8e1ff to 73f04f4 Compare June 4, 2026 23:30
easel pushed a commit to easel/lucebox-hub that referenced this pull request Jun 5, 2026
@easel easel force-pushed the feat/lucebox-cli branch 2 times, most recently from bb7bb11 to a56b51b Compare June 8, 2026 22:08
easel added a commit to easel/lucebox-hub that referenced this pull request Jun 8, 2026
Containerization stack for lucebox-hub. Dockerfile + docker-bake.hcl
build the lucebox-hub image (build-env and runtime stages);
scripts/build_image.sh drives local builds; server/scripts/entrypoint.sh
emits IMAGE_INFO / HOST_INFO sidecars consumed by /props. GitHub Actions
add .github/workflows/docker.yml (build & publish), update ci.yml, and
add release-luce-bench.yml for tagging. Workspace-root files
(pyproject.toml, uv.lock, Makefile, lefthook.yml, .gitignore, README)
live here because the Dockerfile uv-syncs the workspace at build time.

Provides the reproducible image and CI pipeline every other split PR
deploys into. Centralizing build/publish here keeps Dockerfile,
entrypoint, and workspace-root pinning in one reviewable change.

- Luce-Org#335 (lucebox-cli): Dockerfile COPYs lucebox/ into the image
- Luce-Org#337 (lucebench-harness): Dockerfile COPYs luce-bench/ into the image
davide221 added a commit to easel/lucebox-hub that referenced this pull request Jun 9, 2026
…R + personal refs

Strip forward-references to lucebox-cli (Luce-Org#335) and luce-bench (Luce-Org#337) plus the
contributor's personal repos so the docker stack stands alone:

- delete .github/workflows/release-luce-bench.yml (luce-bench PyPI publish;
  fires only on luce-bench-v* tags, needs a luce-bench/ dir not in this repo)
- Makefile: drop test/smoke/bench/profile targets (invoke lucebench/lucebox
  modules absent here) and their now-unused vars
- .gitignore: drop luce-bench/snapshots and external baseline-repo URLs
- pyproject.toml / Dockerfile / docker.yml: de-reference Luce-Org#335/Luce-Org#337/luce-bench
  in comments

No functional change: deps, workspace members, ruff config, and every build
instruction are untouched, so CI stays green. The siblings re-add their own
scaffolding when they land.

Co-Authored-By: WOZCODE <contact@withwoz.com>
@easel easel force-pushed the feat/lucebox-cli branch 2 times, most recently from 5a7e617 to c46e358 Compare June 11, 2026 15:15

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lucebox/src/lucebox/docker_run.py">

<violation number="1" location="lucebox/src/lucebox/docker_run.py:231">
P3: Several newly added helper functions are dead code (defined but never called anywhere in the codebase).</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread scripts/test_lucebox_sh.sh
@easel easel force-pushed the feat/lucebox-cli branch 3 times, most recently from a731f1d to 71139a5 Compare June 16, 2026 17:22
easel added a commit to easel/lucebox-hub that referenced this pull request Jun 16, 2026
…oad core

Defer second-order features to follow-up PRs so this PR is just the host
wrapper and the CLI needed to install, launch/serve, configure, and
download models. Cuts the PR from ~11.2k to ~6.2k added lines.

Deferred out of this PR (land as stacked follow-ups):
- Agent-client adapters + bench: the entire net-new harness/ package
  (bench.py, the six clients, run_lucebench.sh) and the client launcher
  verbs. harness/ reverts to main's loose-scripts state; the root
  pyproject no longer adds harness as a workspace member/dep.
- Autotune sweep + profiles: candidate_configs, the Profile registry and
  per-arch brackets, sweep.py, and the `autotune` command. The host-derived
  DFLASH_* heuristic (runtime_from_host) STAYS — config.live_config needs it
  to bake serve defaults — slimmed into autotune.py.
- profile + smoke commands and their modules.

Coupling fixes:
- recommend_preset moved autotune.py -> download.py (models sub-app uses it).
- Wrapper: drop the autotune --sweep exec-routing special case and trim
  usage/completion/exec-set to the core verbs.

Tests/CI follow the surface: deferred-feature tests removed; runtime_from_host
heuristic tests kept; new guard asserts the deferred verbs are NOT registered.
CI pytest step scoped to lucebox. lucebox 72 passed, wrapper 53 passed,
ruff + mypy + shellcheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 37 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lucebox/src/lucebox/docker_run.py">

<violation number="1" location="lucebox/src/lucebox/docker_run.py:231">
P3: Several newly added helper functions are dead code (defined but never called anywhere in the codebase).</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread lucebox.sh
@easel easel force-pushed the feat/lucebox-cli branch from f3db094 to d9ca9f8 Compare June 16, 2026 23:09
easel added a commit to easel/lucebox-hub that referenced this pull request Jun 16, 2026
Stacked on the core lucebox CLI (Luce-Org#335). Restores the second-order tuning
and diagnostics surface deferred from that PR:

- autotune.py: the empirical sweep machinery — candidate_configs, the
  Profile registry + per-arch coding-agent-loop brackets, get_profile.
  (recommend_preset stays in download.py from the core split.)
- sweep.py: the per-cell config sweep driver (config set -> restart ->
  luce-bench snapshot -> winner pick).
- profile.py / smoke.py + the `autotune`, `profile`, `smoke` CLI commands.
- Wrapper: the `autotune --sweep` exec-routing special case (sweeps must
  stay on docker run, not exec into the container they'd restart) and the
  autotune/profile/smoke entries in usage + completion + the exec set.

Tests: sweep / profile / smoke / autotune-cli / candidate-configs suites
and the Profile/bracket tests restored; test_cli guards that the client
launcher verbs are still deferred while autotune/profile/smoke register.

lucebox 122 passed, wrapper 55 passed, ruff + mypy + shellcheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
easel added a commit to easel/lucebox-hub that referenced this pull request Jun 16, 2026
Stacked on the tuning PR (which is stacked on core Luce-Org#335). Restores the
last deferred cluster: the agent-client launch surface.

- harness/ becomes an installable package again (bench.py + the six client
  adapters: claude_code, codex, hermes, openclaw, opencode, pi, plus
  _common and run_lucebench.sh) and rejoins the uv workspace.
- cli.py: the claude/codex/opencode/hermes/pi/openclaw verbs, registered
  from one factory, plus _detect_server_url / _exec_client (which reuse
  profile._server_base_urls from the tuning PR — hence this stacks on it).
- CI runs the harness pytest suite alongside lucebox again.

recommend_preset stays in download.py (core's relocation), so the models
sub-app is unaffected.

lucebox + harness 156 passed, wrapper 55 passed, ruff + mypy + shellcheck
clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
easel added a commit to easel/lucebox-hub that referenced this pull request Jun 17, 2026
Stacked on the core lucebox CLI (Luce-Org#335). Restores the second-order tuning
and diagnostics surface deferred from that PR:

- autotune.py: the empirical sweep machinery — candidate_configs, the
  Profile registry + per-arch coding-agent-loop brackets, get_profile.
  (recommend_preset stays in download.py from the core split.)
- sweep.py: the per-cell config sweep driver (config set -> restart ->
  luce-bench snapshot -> winner pick).
- profile.py / smoke.py + the `autotune`, `profile`, `smoke` CLI commands.
- Wrapper: the `autotune --sweep` exec-routing special case (sweeps must
  stay on docker run, not exec into the container they'd restart) and the
  autotune/profile/smoke entries in usage + completion + the exec set.

Tests: sweep / profile / smoke / autotune-cli / candidate-configs suites
and the Profile/bracket tests restored; test_cli guards that the client
launcher verbs are still deferred while autotune/profile/smoke register.

lucebox 122 passed, wrapper 55 passed, ruff + mypy + shellcheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
easel added a commit to easel/lucebox-hub that referenced this pull request Jun 17, 2026
Stacked on the tuning PR (which is stacked on core Luce-Org#335). Restores the
last deferred cluster: the agent-client launch surface.

- harness/ becomes an installable package again (bench.py + the six client
  adapters: claude_code, codex, hermes, openclaw, opencode, pi, plus
  _common and run_lucebench.sh) and rejoins the uv workspace.
- cli.py: the claude/codex/opencode/hermes/pi/openclaw verbs, registered
  from one factory, plus _detect_server_url / _exec_client (which reuse
  profile._server_base_urls from the tuning PR — hence this stacks on it).
- CI runs the harness pytest suite alongside lucebox again.

recommend_preset stays in download.py (core's relocation), so the models
sub-app is unaffected.

lucebox + harness 156 passed, wrapper 55 passed, ruff + mypy + shellcheck
clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
easel added a commit to easel/lucebox-hub that referenced this pull request Jun 17, 2026
Stacked on the tuning PR (which is stacked on core Luce-Org#335). Restores the
last deferred cluster: the agent-client launch surface.

- harness/ becomes an installable package again (bench.py + the six client
  adapters: claude_code, codex, hermes, openclaw, opencode, pi, plus
  _common and run_lucebench.sh) and rejoins the uv workspace.
- cli.py: the claude/codex/opencode/hermes/pi/openclaw verbs, registered
  from one factory, plus _detect_server_url / _exec_client (which reuse
  profile._server_base_urls from the tuning PR — hence this stacks on it).
- CI runs the harness pytest suite alongside lucebox again.

recommend_preset stays in download.py (core's relocation), so the models
sub-app is unaffected.

lucebox + harness 156 passed, wrapper 55 passed, ruff + mypy + shellcheck
clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@davide221 davide221 requested a review from dusterbloom June 24, 2026 09:47
@easel easel force-pushed the feat/lucebox-cli branch from aeae314 to 0ae47c5 Compare July 6, 2026 01:32
easel added a commit to easel/lucebox-hub that referenced this pull request Jul 6, 2026
…oad core

Defer second-order features to follow-up PRs so this PR is just the host
wrapper and the CLI needed to install, launch/serve, configure, and
download models. Cuts the PR from ~11.2k to ~6.2k added lines.

Deferred out of this PR (land as stacked follow-ups):
- Agent-client adapters + bench: the entire net-new harness/ package
  (bench.py, the six clients, run_lucebench.sh) and the client launcher
  verbs. harness/ reverts to main's loose-scripts state; the root
  pyproject no longer adds harness as a workspace member/dep.
- Autotune sweep + profiles: candidate_configs, the Profile registry and
  per-arch brackets, sweep.py, and the `autotune` command. The host-derived
  DFLASH_* heuristic (runtime_from_host) STAYS — config.live_config needs it
  to bake serve defaults — slimmed into autotune.py.
- profile + smoke commands and their modules.

Coupling fixes:
- recommend_preset moved autotune.py -> download.py (models sub-app uses it).
- Wrapper: drop the autotune --sweep exec-routing special case and trim
  usage/completion/exec-set to the core verbs.

Tests/CI follow the surface: deferred-feature tests removed; runtime_from_host
heuristic tests kept; new guard asserts the deferred verbs are NOT registered.
CI pytest step scoped to lucebox. lucebox 72 passed, wrapper 53 passed,
ruff + mypy + shellcheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
easel and others added 10 commits July 6, 2026 14:29
… adapters

New lucebox/ Python package exposing the hub CLI (autotune, sweep,
profile, smoke, models, config, download, host-check, docker_run) plus
the lucebox.sh launcher wrapper and install.sh. Adds the harness/
adapter package wrapping external coding agents (claude_code, codex,
hermes, openclaw, opencode, pi) that autotune sweeps drive. Ships
scripts/check_lucebox_wrapper_sandbox.sh and scripts/test_lucebox_sh.sh
for wrapper validation, full pytest coverage under lucebox/tests/, and
the bragi autotune profile-sweep protocol docs.

This is the user-facing surface of lucebox-hub: one CLI to launch the
image, tune layer-split / pflash settings against a host, run sweeps,
and dispatch bench runs. Splitting it out keeps Python-side review
independent of the C++ server and Docker stack reviews.

- Luce-Org#334 (docker-stack): docker_run.py launches the lucebox-hub image
- Luce-Org#337 (lucebench-harness): lucebox bench delegates to luce-bench (workspace dep)
- Luce-Org#336 (server-layer-split): autotune presumes layer-split build artifacts
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…to-auto-pick

entrypoint.sh:write_host_info() bailed loudly when /opt/lucebox-hub/ did not
exist on the host (unit tests, plain docker run without bind mount), because
bash refuses the > redirect before the command runs and 2>/dev/null does not
suppress the redirect's own error. Guard with an upfront [ -d ] check.

test_lucebox_sh.sh:test_entrypoint_multi_target was asserting against the
pre-Luce-Org#334 multi-target semantics (auto-pick + warn + exec shim). PR Luce-Org#334
(merged) changed that to refuse-to-auto-pick + exit non-zero. Update the
assertion: still drives the auto-detect block (so any DRAFT_FAMILY_GLOB
set -u regression trips), but now requires the refuse warn to fire and the
shim to NOT exec.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the missing rc != 0 assertion to test_entrypoint_multi_target. The
previous fix updated the test to look for the "Refusing to auto-select"
warn and a non-exec'd shim, but didn't check the exit code. A regression
where the entrypoint logged the warn but still exited 0 (silently
auto-picking under the covers) would have slipped through. The container
MUST fail to start on multi-target ambiguity — that is the whole point
of the policy added in Luce-Org#334.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…e CI

Behavior-preserving cleanup of the lucebox hub CLI surfaced by review:

- cli.py: delete dead _pick_variant_from_driver; collapse the six
  near-identical Typer client subcommands into a factory loop.
- harness adapters: rewire claude_code onto _common; add build_base_parser
  + run_main so the six main() arg-parsers stop duplicating (per-client
  differences preserved); hoist KNOWN_AREAS in bench.py.
- Unify DFLASH_ALLOWLIST in types.py (was duplicated with a silent
  one-field divergence between cli.py and sweep.py).
- config.py: extract _atomic_write_doc.
- Shell: shared _append_host_env / _append_scalar_env / _set_tty_flags
  helpers in lucebox.sh, _trim in entrypoint.sh, sandbox factory in the
  test runner.
- FIX a tty regression introduced while sharing the docker interactive
  flags: the helper ran behind `< <(...)`, so `[ -t 1 ]` tested the pipe
  and forced -i even on a real tty (breaking interactive client TUIs).
  Now runs in caller scope via nameref; added a PTY-based regression test.
- Add unit tests for the previously-untested harness adapters + bench, and
  wire lucebox + harness pytest into CI (it previously never ran pytest).

116 -> 156 tests; bash wrapper 54 -> 55; ruff + mypy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oad core

Defer second-order features to follow-up PRs so this PR is just the host
wrapper and the CLI needed to install, launch/serve, configure, and
download models. Cuts the PR from ~11.2k to ~6.2k added lines.

Deferred out of this PR (land as stacked follow-ups):
- Agent-client adapters + bench: the entire net-new harness/ package
  (bench.py, the six clients, run_lucebench.sh) and the client launcher
  verbs. harness/ reverts to main's loose-scripts state; the root
  pyproject no longer adds harness as a workspace member/dep.
- Autotune sweep + profiles: candidate_configs, the Profile registry and
  per-arch brackets, sweep.py, and the `autotune` command. The host-derived
  DFLASH_* heuristic (runtime_from_host) STAYS — config.live_config needs it
  to bake serve defaults — slimmed into autotune.py.
- profile + smoke commands and their modules.

Coupling fixes:
- recommend_preset moved autotune.py -> download.py (models sub-app uses it).
- Wrapper: drop the autotune --sweep exec-routing special case and trim
  usage/completion/exec-set to the core verbs.

Tests/CI follow the surface: deferred-feature tests removed; runtime_from_host
heuristic tests kept; new guard asserts the deferred verbs are NOT registered.
CI pytest step scoped to lucebox. lucebox 72 passed, wrapper 53 passed,
ruff + mypy + shellcheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…utotune)

The exhaustive VRAM-tier test for recommend_preset lived in the deferred
test_autotune_cli.py; since the function moved to download.py in the split,
its direct test belongs alongside it. The models auto-recommend path is
also covered indirectly by test_models_cli.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eftovers

The serve-argv builder (docker_run.py) — the core's whole job — was only
50% covered by one test. Add lucebox/tests/test_docker_run.py pinning the
argv/printable contract, every DFLASH_* env (always-on + conditional),
target/draft path resolution, host-env forwarding, volume mounts, and
docker_pull. docker_run.py 50% -> 96%.

Includes a regression guard for the preset-cap analysis: a large preset
serves at the conservative DflashRuntime() floor (max_ctx=16384), not a
high OOM-prone ctx — the VRAM-tier heuristic's higher caps only apply via
`autotune --apply` (which threads cfg.model.preset; separate PR). No bug.

Also:
- Add lucebox/**/*.py to the ruff include set (was mypy-checked but never
  ruff-linted) and clear the 3 unused imports it surfaced in cli.py.
- Drop an orphan `# autotune` section divider and refresh two docstrings
  that referenced the deferred autotune subcommand.

lucebox 89 passed, wrapper 53 passed, ruff + mypy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Core carried three bits only the deferred follow-ups use. Evict them so
the core CLI is just the basic launch/config/download surface:

- types.BASE_DFLASH_ALLOWLIST — no core caller; used by the sweep's
  per-cell bracket. Moves to the tuning PR.
- autotune.runtime_from_host preset-awareness (_preset_approx_gb + the
  ≥20 GB large-model 32K branch) — core always calls runtime_from_host(host)
  with no preset, so this was unreachable here. The preset-size cap moves
  to the tuning PR alongside autotune --apply (which threads cfg.model.preset).
  Behavior for the core's preset-less call is unchanged.
- config.live_config(preset_name=...) — dead across the entire tree (every
  call site passes no arg). Deleted outright, not moved.

Their tests move with them. lucebox 87 passed, wrapper 53, ruff + mypy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The lucebox package is a uv workspace member (root pyproject + uv.lock), so
`uv sync --frozen` in the runtime stage fails ("Distribution not found at
file:///opt/lucebox-hub/lucebox") without its source present. The Dockerfile
carried a "copy later" TODO from the docker-stack PR; complete it here since
the CLI is meant to run in-container (the host wrapper docker-execs into it).

Adds COPY lucebox to the builder stage and COPY --from=builder to runtime.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@easel easel force-pushed the feat/lucebox-cli branch from 0ae47c5 to 573570a Compare July 6, 2026 19:05
easel and others added 2 commits July 6, 2026 15:12
_serve_stop runs as the EXIT trap; if the server fast-fails, the trap can
fire after _serve_and_track has unwound, so its `local docker_pid` is out of
scope and `wait "$docker_pid"` trips `set -u` ("docker_pid: unbound
variable"), turning cleanup into a spurious exit-1 that fails the systemd
unit and masks the real startup error. Guard with ${docker_pid:-}.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tStatus)

`serve` traps SIGTERM, stops the container, and exits 143 (128+SIGTERM), so a
normal `systemctl --user stop lucebox` left the unit in "failed" state instead
of "inactive". Add SuccessExitStatus=143 SIGTERM to the generated unit so a
clean stop is reported as success; genuine crashes exit with other codes and
still trip Restart=on-failure. Guarded by the unit-directive test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lucebox.sh">

<violation number="1" location="lucebox.sh:696">
P2: The `SuccessExitStatus=143 SIGTERM` directive in the generated systemd unit treats ALL exits with code 143 as successful, which means `Restart=on-failure` will not restart the service after unexpected terminations like an external `docker stop` or daemon-initiated container SIGTERM. These scenarios produce the same 143 exit code as an intentional `systemctl stop` but should trigger a restart. A better approach would be to make `_serve_and_track` distinguish intentional signal-driven stops (e.g., by tracking when the trap fires and returning 0 instead of 143 on clean shutdown) so that `SuccessExitStatus=143` can be removed and only genuine unexpected failures trigger a restart.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread lucebox.sh
# \`serve\` traps SIGTERM, stops the container, and exits 143 (128+SIGTERM).
# Without this, a normal \`systemctl stop\` leaves the unit "failed" instead of
# "inactive". Genuine crashes exit with other codes and still trip Restart.
SuccessExitStatus=143 SIGTERM

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: The SuccessExitStatus=143 SIGTERM directive in the generated systemd unit treats ALL exits with code 143 as successful, which means Restart=on-failure will not restart the service after unexpected terminations like an external docker stop or daemon-initiated container SIGTERM. These scenarios produce the same 143 exit code as an intentional systemctl stop but should trigger a restart. A better approach would be to make _serve_and_track distinguish intentional signal-driven stops (e.g., by tracking when the trap fires and returning 0 instead of 143 on clean shutdown) so that SuccessExitStatus=143 can be removed and only genuine unexpected failures trigger a restart.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lucebox.sh, line 696:

<comment>The `SuccessExitStatus=143 SIGTERM` directive in the generated systemd unit treats ALL exits with code 143 as successful, which means `Restart=on-failure` will not restart the service after unexpected terminations like an external `docker stop` or daemon-initiated container SIGTERM. These scenarios produce the same 143 exit code as an intentional `systemctl stop` but should trigger a restart. A better approach would be to make `_serve_and_track` distinguish intentional signal-driven stops (e.g., by tracking when the trap fires and returning 0 instead of 143 on clean shutdown) so that `SuccessExitStatus=143` can be removed and only genuine unexpected failures trigger a restart.</comment>

<file context>
@@ -690,6 +690,10 @@ Wants=network-online.target docker.service
+# \`serve\` traps SIGTERM, stops the container, and exits 143 (128+SIGTERM).
+# Without this, a normal \`systemctl stop\` leaves the unit "failed" instead of
+# "inactive". Genuine crashes exit with other codes and still trip Restart.
+SuccessExitStatus=143 SIGTERM
 Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
 Environment=LUCEBOX_IMAGE=$IMAGE_BASE
</file context>

A fresh install did not actually auto-tune: `models download --activate`
writes only [model], and load() fills a config.toml with no [dflash] from
DflashRuntime class defaults (max_ctx=16384), ignoring the host tier. The
live_config heuristic only fires when there is NO config.toml at all — which
stops being true the moment a model is activated. Net effect: a 24 GB card
served at 16 K instead of the ~98 K it can handle.

Add config.seed_dflash_from_host(host): on first activation it derives the
VRAM-tier DflashRuntime and writes it (plus an [autotune] source=heuristic
stamp) to config.toml. No-op when a [dflash] section already exists, so it
never clobbers a prior tune or user edits. Wired into `models download
--activate`.

Verified: fresh [model]-only config now serves max_ctx=98304 (was 16384) on
a 24 GB host. lucebox 89 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lucebox/src/lucebox/config.py">

<violation number="1" location="lucebox/src/lucebox/config.py:355">
P2: The `seed_dflash_from_host` function can accidentally block legacy `.env` migration. Because it calls `load_doc()` directly—which returns `{}` for a missing file and never checks for a legacy `.env`—it may create a brand-new `config.toml` containing only the heuristic dflash keys. Once that `config.toml` exists, the standard `load()` path will skip migration forever, silently dropping any legacy overrides (port, image, models_dir, prior DFLASH values, etc.). The current CLI call site happens to call `_load_or_build()` first, but the function is a standalone public API and this is a latent data-loss hazard for any future caller or refactor. Consider running the legacy migration check inside `seed_dflash_from_host` before calling `load_doc()`, or calling `load(path)` upfront to ensure any legacy `.env` is promoted first.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

import lucebox.autotune as autotune_mod

path = path or default_config_path()
doc = load_doc(path)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: The seed_dflash_from_host function can accidentally block legacy .env migration. Because it calls load_doc() directly—which returns {} for a missing file and never checks for a legacy .env—it may create a brand-new config.toml containing only the heuristic dflash keys. Once that config.toml exists, the standard load() path will skip migration forever, silently dropping any legacy overrides (port, image, models_dir, prior DFLASH values, etc.). The current CLI call site happens to call _load_or_build() first, but the function is a standalone public API and this is a latent data-loss hazard for any future caller or refactor. Consider running the legacy migration check inside seed_dflash_from_host before calling load_doc(), or calling load(path) upfront to ensure any legacy .env is promoted first.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lucebox/src/lucebox/config.py, line 355:

<comment>The `seed_dflash_from_host` function can accidentally block legacy `.env` migration. Because it calls `load_doc()` directly—which returns `{}` for a missing file and never checks for a legacy `.env`—it may create a brand-new `config.toml` containing only the heuristic dflash keys. Once that `config.toml` exists, the standard `load()` path will skip migration forever, silently dropping any legacy overrides (port, image, models_dir, prior DFLASH values, etc.). The current CLI call site happens to call `_load_or_build()` first, but the function is a standalone public API and this is a latent data-loss hazard for any future caller or refactor. Consider running the legacy migration check inside `seed_dflash_from_host` before calling `load_doc()`, or calling `load(path)` upfront to ensure any legacy `.env` is promoted first.</comment>

<file context>
@@ -334,6 +335,39 @@ def save(cfg: Config, path: Path | None = None, *, doc: dict[str, Any] | None =
+    import lucebox.autotune as autotune_mod
+
+    path = path or default_config_path()
+    doc = load_doc(path)
+    if "dflash" in doc:
+        return False
</file context>

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.

1 participant