Skip to content

feat(app): the harness switcher in the chat box (#1549) - #1550

Open
aarontrowbridge wants to merge 1 commit into
mainfrom
amico/1549-harness-switcher
Open

aarontrowbridge wants to merge 1 commit into
mainfrom
amico/1549-harness-switcher

Conversation

@aarontrowbridge

Copy link
Copy Markdown
Member

Closes #1549.

What landed, per layer

Engine route (packages/app-bundle/overlay/packages/opencode/src/server/amicode/harness.ts + registration in the instance httpapi server):

  • GET /amicode/harness serves {ok, harness, status, options?} — the current harness plus the extension-published registry view (harness-options.json in the ops dir). The engine never computes or hardcodes the registry; a response without options means "no registry published" and the composer control stays hidden (stock-opencode behavior, unchanged).
  • POST /amicode/harness ({harness}) validates the id against that published view (it IS the allowlist), no-ops when already current (the watcher is never poked for nothing), and atomically writes {harness, status:"switching"} — the same loopback guard, sibling ok/false shape, and fixed value-free refusals as the solver-mode flip.

Extension (packages/extension/src/harness_switch.ts, harness.ts, extension.ts):

  • harnessMenu — the ONE serialization of the registry both fronts render; decideHarnessSwitch — the ONE gate both consult. Unknown ids, needs-setup, and unentitled targets are blocked with their reason; the harness.telaio entitlement read-side gate lives here (disabled-with-reason), not as a hidden spawn-time failure.
  • watchHarnessSwitch — the solver watcher's poll + busy-latch discipline over harness.json; the watcher performs the real switch (gate → amicode.harness setting persist → menu republish → amicode.restartServer) and settles status:"ready". A refused or failed switch settles ready at the still-current harness — never a ready file claiming a switch that didn't happen.
  • The amicode.selectHarness palette command now renders from the same menu and re-gates through the same decision at confirm time; boot publishes the options file (best-effort, never blocks boot).

Composer (app + session-ui overlay):

  • A fourth select-slot (harnessControl) beside agent/model/variant. Entries carry the honest state; disabled entries show their reason; the disclosure copy — "Sessions are harness-local — switching harnesses switches session history" — rides every selectable entry at the point of switching.
  • harnessDegraded: under a non-opencode harness the agent/model/variant slots dim inertly with a "Not accepted by this harness" affordance (Harness Contract v1 accepts none); display: contents otherwise — layout byte-identical.
  • The harness switch banner (solver banner's twin) narrates switching → restarting → ready, speaks only for switches the app requested, and carries the same stall/ceiling expiry discipline.

Verification (commands + counts verbatim)

  • bunx vitest run test/harness.test.ts (extension) — 32 passed (incl. the new menu/gate/two-fronts pin tests)
  • bunx vitest run test/harness_switch.test.ts (extension) — 12 passed
  • bunx vitest run test/scores (extension, untouched) — 20 files / 323 passed (expected count matches)
  • bun run typecheck (packages/extension) — clean
  • bun test test/server/amicode-harness.test.ts (materialized engine tree) — 12 passed, 48 expect() calls
  • bun test test/server/amicode-connections.test.ts (solver-mode neighbors, materialized) — 103 passed
  • bun test --conditions=solid src/components/harness-switch.test.ts src/components/prompt-input/harness.test.ts (materialized app) — 11 passed
  • bash packages/app-bundle/scripts/engine_typecheck_gate.sh — OK (only the Base-pin drift: manifest upstream_base_sha stale vs absorbed overlay #1229-tracked base-drift allowlist errors)
  • bash packages/app-bundle/scripts/overlay_typecheck_gate.sh — OK across 7 packages (app ui session-ui core schema sdk/js llm)
  • node packages/app-bundle/scripts/drift_gate.mjs — PASS (manifest refreshed for the new/changed overlay files, 859 files)
  • bun test --conditions=browser src (materialized session-ui) — 433 pass / 1 fail: the editable-diff-view selection-highlight test fails identically with this slice's changes reverted (pre-existing on this base, unrelated CSS topic)
  • bunx vitest run (extension full suite) — 3689 passed / 16 failed across 8 files, all pre-existing: the same 8 files (amicode_service_* auth/bootstrap/proxy/wiring + terminal) fail identically at HEAD without this slice (environment-dependent, need a built dist)
  • bun test --conditions=solid --preload ./happydom.ts ./src (materialized app) — 1173 pass / 19 fail + 2 errors, all pre-existing: identical failures with the slice's changed files reverted (i18n parity, sync reducers, editor-dom, offset observers)

Notes

  • One registry, two fronts is pinned by test: both flows are decideHarnessSwitch → persist; the same inputs land the same persisted harness from either front, and a blocked request persists nothing from either.
  • No schema changes, no new session-identity fields; sessions stay harness-local by the existing restart semantics.

The composer gets a fourth select-slot — the harness — riding the
solver-mode trio end to end:

- engine: GET/POST /amicode/harness (server/amicode/harness.ts). The id
  allowlist IS the extension-published registry view
  (harness-options.json) — the engine never hardcodes harness identity;
  POST validates against it, no-ops when already current, and writes
  {harness, status:"switching"} to the ops dir (same loopback guard +
  fixed value-free refusals as the solver-mode flip).
- extension: harness_switch.ts (the handshake reader/writer + the
  watcher, the solver watcher's poll+busy-latch discipline) and the
  registry layer (harness.ts): harnessMenu — the ONE serialization both
  fronts render — and decideHarnessSwitch — the ONE gate both consult
  (unknown/needs-setup/unentitled blocked with their reason; the
  harness.telaio read-side gate lives here, never as a spawn-time
  failure). The palette command now renders from the same menu and gates
  through the same decision; the watcher persists the setting, republishes
  the menu, restarts, and settles ready — a refused switch settles ready
  at the STILL-CURRENT harness, never a lie.
- composer: the harness select (disabled-with-reason entries carrying the
  disclosure "switching harnesses switches session history" at the point
  of switching), harnessDegraded dimming of agent/model/variant under a
  non-opencode harness (Harness Contract v1 accepts none), and the
  harness switch banner narrating switching → restarting → ready.

Under opencode, byte-for-byte unchanged when untouched: no published
registry → the control renders nothing.
@coderabbitai

coderabbitai Bot commented Sep 24, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 41bb141c-8397-44bd-b085-454843f6717f


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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.

feat(app): the harness switcher in the chat box — composer control riding the solver-mode trio, entitlement-gated, harness-aware degradation

1 participant