Skip to content

feat: hand — the ninth Colleague role and the fleet's fine-tuning base, on LiquidAI LFM2.5-1.2B (0.56.1) - #184

Merged
OriNachum merged 17 commits into
mainfrom
feat/hand-lobe-lfm2.5-1.2b
Aug 10, 2026
Merged

feat: hand — the ninth Colleague role and the fleet's fine-tuning base, on LiquidAI LFM2.5-1.2B (0.56.1)#184
OriNachum merged 17 commits into
mainfrom
feat/hand-lobe-lfm2.5-1.2b

Conversation

@OriNachum

Copy link
Copy Markdown
Contributor

What

Adds hand — the fleet's ninth Colleague role and its fine-tuning base — on LiquidAI/LFM2.5-1.2B-Instruct.

The metaphor is muscle memory: one cheap base, many LoRA adapters, each mastering a domain. worker is an untrained generalist doer; hand is a trained specialist. At ~2.4 GiB of bf16 weights it is cheap enough to be hosted by every built-in shape, which is what makes three further properties fall out:

  • it is the minor/cheap tier, displacing Qwen/Qwen3.5-4B (which stays in the catalog as a plain candidate — cite-don't-delete);
  • it is the pressure-policy servable floor — an explicit hand request is always served, even when cortex/senses/worker/muse are shedding 429s;
  • it is never proxied. That absence is named by a constant, NEVER_PROXIED_BACKENDS, so a symmetry-minded refactor has to delete something to break it rather than quietly add a fourth peer channel.

Responsibilities: domain_mastery, learned_skill, specialized_task, tool_use. Forbidden: final_decision, repo_action, security_decision. v1 withholds repo_action deliberately — adding a responsibility later is contract-compatible, removing one is a break. Granting it once adapters exist is #180.

Full method chain: /scope/think/challenge/spec-to-plan → in-house build → /deviate ×10 → /summarize-delivery. Spec, plan and delivery summary are all in-tree.

The checkpoint, and three flags that are absent on purpose

Lfm2ForCausalLM — a ~1.2B hybrid short-conv + GQA, 32768 native, bf16. Requires vLLM ≥ 0.23.0, so the lane pins the nightly digest rather than the NGC 26.04 tag vllm-minor rides.

Three lane flags are deliberately omitted, each for a reason a reviewer should be able to check:

absent flag why
--quantization the catalog's quantization="none" bf16 sentinel. Passing it empty is a parse error; passing modelopt corrupts the weights. Live engine confirms quantization=None
--language-model-only text-only checkpoint, no ViT. LiquidAI ships the vision variant separately
--reasoning-parser no thinking mode. LFM2.5-1.2B-Thinking is a separate checkpoint. Arming a reasoning parser here is arming half a pair with no other half

Tool calling — the gemma4 trap, avoided

LFM2's <|tool_call_start|> / <|tool_call_end|> are special tokens. That is precisely the trap that made pythonic silently wrong for Gemma 4: the parser ran with skip_special_tokens=True, matched nothing, and vLLM relayed a well-formed tool call as ordinary assistant content with tool_calls: null. Callers got prose shaped like a tool call.

vLLM ships a purpose-built lfm2 parser whose __init__ resolves both delimiters and raises when either is missing — so a bad tokenizer revision fails loudly at startup instead of degrading to prose. Strictly better than the failure mode we already paid for once.

Confirmed live on two cards, not assumed:

finish_reason: "tool_calls"
tool_calls: [{"function": {"name": "get_weather", "arguments": "{\"city\": \"Paris\"}"}}]

…with the tokenizer delimiters verified present as ids 10 / 11.

LoRA — armed, inventory empty

--enable-lora ships armed with zero adapters. That is the honest v1 shape: the serving half of muscle memory lands now, the training half is cross-repo (agentculture/unsloth-cli#16), and an armed-but-empty lane is more honest than a promised-later one.

Adapters are declared once, in HAND_LORA_MODULES (name=path, comma-separated), read by both the engine's --lora-modules and the gateway's hand:<domain> alias derivation — so the two cannot disagree. Fixed at boot; no runtime hot-load.

  • model=hand serves the base and never 404s on an empty inventory
  • model=hand:<domain> serves that adapter
  • an undeclared hand:<domain> is refused model_not_found — never silently downgraded to the base

Adapter honesty is enforced by probing the engine's own /v1/models, not the gateway's filesystem — adapter paths are mounted into vllm-hand, not into the gateway, so the filesystem answer would be a lie.

Live results — a deliberately split verdict

Three physical cards. Functional results pass on two; no card's budget is validated, and that is a claim this PR makes rather than hides.

card functional budget evidence
Orin (sm_87, 64 GB) PASS not reproducible docs/evidence/2026-08-10-partial-hand-orin.txt
Spark (GB10, 128 GB) PASS not reproducible docs/evidence/2026-08-10-partial-hand-spark.txt
Thor (sm_110, 128 GB) boot failed #181
base untestable by construction

Functional coverage on the two serving cards: architecture resolved, bf16 sentinel, no reasoning parser, LoRA armed, correct known-answer, structured tool_calls, undeclared adapter 404, /v1/models = base only, compose healthy.

Why no budget reproduces

Three Spark boots, minutes apart, identical lane, identical gpu_mem_util = 0.06:

run free RAM before available KV KV tokens
1 ~28 GiB 6.21 GiB 541,886
2 ~18 GiB 3.34 GiB 291,970
3 ~18 GiB 3.54 GiB 308,754

Runs 2 and 3 agree within 6% at the same free-memory level; run 1 followed 31.7 GiB being freed on the box. That correlation is what turns three scattered numbers into a mechanism:

On a unified-memory card with co-resident tenants, gpu_mem_util does not name a stable budget. vLLM profiles against memory free at that instant, so the same util yields a different KV pool run to run. A single boot's KV number measures the box's state, not the card's capacity for the role.

This is not a tight-margin artifact: the Orin showed it with ~1 GiB of margin on a 61 GiB card, the Spark with several GiB on a 121 GiB one. hand booted and served at 0.06 on every run of both cards — what is unsupportable is any measured pool or concurrency figure. All four cards stay DECLARED (#183).

Two defects the live runs caught that no offline test could

  1. Missing cudagraph off-switch (d5). The lane was the only one of eight not setting VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS=0. Live consequence: Available KV cache memory: -9.25 GiB — negative, unbootable at any util. The rendered compose was perfectly valid right up until an engine profiled memory with it. No cross-lane parity test exists; filed as No test asserts compose-lane env parity — the hand lane shipped without the cudagraph off-switch every sibling sets #182.

  2. A dead knob (d8). HAND_ATTENTION_BACKEND was rendered into .env by a card profile and substituted by nothing — the operator reads a configured attention backend, the engine never receives one. Found only by booting the real committed lane; earlier hand-rolled docker run probes set the backend manually and so disagreed with the lane. Fixed via --attention-config, and guarded by a new class-level test asserting every profile-rendered key is substituted by the fleet template — verified to FAIL with the fix reverted.

A retraction, kept in the history

I wrote an Orin acceptance transcript after one successful boot and claimed VALIDATED. Two later boots at identical settings refused to start. The transcript was renamed …-partial-…, carries a retraction at its head, and the card profile, per-model doc, machine-profiles and delivery summary were all changed back to DECLARED (d9). The Spark runs subsequently explained that non-reproduction rather than excusing it. The retraction stays in the branch history on purpose — it is the reasoning that produced the mechanism above.

Review pointers

  • lobes/catalog.pyTIER_ROLE order is load-bearing: tier_aliases derives ascending capability from each role's last occurrence (hand < multimodal < worker < muse < primary). Reordering silently changes the tier ladder.
  • lobes/roles.pyGATEWAY_FRONTED_ROLES is now derived from ROLE_ROLE_HINT rather than hand-typed. This fixed a real half-land: the new role was registered in all six tables and still absent from the registry. Same defect class as the 0.54.8 worker peer-proxy bug, which is why the per-table completeness tests are parametrised over ROLES.
  • lobes/templates/mg-logwrap.sh — drops any --*= argument, because a compose command list cannot omit an argument conditionally and --lora-modules= must not reach vLLM. Confirmed absent from the post-strip argv on all three machines.
  • lobes/minor/governance.py — duty lists were re-derived and deliberately left unchanged: competence is not authority. It also names no model ids, an invariant its own test asserts.

Testing

  • 2893 pass, 15 skipped (live gates)
  • black / isort / flake8 / bandit / markdownlint / afi cli doctor . --strict all clean
  • 28 shape×card goldens regenerated — byte-diff shows HAND_* additions only, zero deletions

Follow-ups

issue what
#180 grant hand repo_action once adapters exist
#181 Thor's boot dies in LoRA embedding-slot allocation; three candidate causes deliberately unseparated
#182 no test asserts compose-lane env parity
#183 no card has a reproducible budget; adapter serving unexercised end to end
agentculture/unsloth-cli#16 LFM2.5 support, so adapters can actually be produced
  • lobes (Claude)

OriNachum and others added 13 commits August 10, 2026 03:10
Converged Announcement Frame for `hand`, a ninth Colleague role: a 1.2B
LFM2.5 base hosted on every machine, carrying many LoRA adapters, each
mastering a specific domain ("muscle memory").

Key decisions: hand is a first-class role, not an unnamed gear; it replaces
`minor` as the cheap tier target, LoRA base and pressure floor (a repointing,
not a removal); v1 serves adapters via --enable-lora but never trains them
(training is owned by unsloth-cli); hand is never proxied and no shape may
drop it.

Verified live against the pinned nightly digest on the Jetson AGX Thor
(vLLM 0.23.1rc1.dev672): Lfm2ForCausalLM is registered, and the tool parser
is `lfm2` — a purpose-built parser for special-token-delimited pythonic
calls, so the generic `pythonic` guess would have failed exactly as it did
for gemma4. Lfm2ForCausalLM declares SupportsLoRA at source.

The /challenge pass surfaced eight findings, notably that every mesh-lobe
shape's GPU budget was measured with no hand co-resident, and that
roles_measure.py would raise KeyError rather than degrade.

Follow-ups: #180 (grant repo_action once adapters
exist), agentculture/unsloth-cli#16 (verify LFM2.5 fine-tuning).

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

Seeds from the converged hand-lobe-lfm2-5-1-2b frame (76 coverage targets:
38 claims + 38 honesty conditions). All 12 tasks confirmed, every target
covered, every task carrying TDD-phrased acceptance criteria and an explicit
FILES: scope line for merge-disjointness.

Waves (devague plan waves):
  wave 0: t1 t2 t3 t4 t5 t6 t7   (file-disjoint, verified by hand)
  wave 1: t8 t9 t11
  wave 2: t10
  wave 3: t12

Deps: t8<-t3,t6 · t9<-t2 · t10<-t8 · t11<-t1,t2,t3,t4 · t12<-t4,t6,t8,t10

Risk r1 (unknown_blocking, budget headroom with hand co-resident) resolved:
t10 is authorised to re-apportion a committed lane budget, measured under
docs/evidence/ per #108; a card with no headroom honestly gets
HAND_FEASIBLE=false per #92. r2/r3 stay open non-blocking, r4 tracks
adapter production in agentculture/unsloth-cli#16.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PcC3i2xBuGcVPPH4tTr5pv
Implements waves 0-1 of docs/plans/2026-08-10-hand-lobe-lfm2-5-1-2b.md.
`hand` is the fleet's designated fine-tuning base: one cheap base, many LoRA
adapters, each mastering a domain ("muscle memory"). Where `worker` is an
untrained generalist doer, `hand` is a trained specialist.

t1 catalog + parser + doc + tier repoint
  - LiquidAI/LFM2.5-1.2B-Instruct: role_hint="hand", 32768 native, bf16
    ("none" sentinel), text-only, no thinking mode.
  - runtime/_parser.py learns `lfm2` — LFM2's <|tool_call_start|> delimiters
    are SPECIAL TOKENS, the same trap that disproved `pythonic` for Gemma 4.
    This parser RAISES on a tokenizer missing them rather than degrading to
    prose. No --reasoning-parser: there is no thinking mode to pair with.
  - minor/cheap tiers repoint to hand; Qwen/Qwen3.5-4B demoted to `candidate`
    (cite-don't-delete, still selectable via `lobes switch`).
  - docs/lfm2.5-1.2b-hand.md.

t2 roles.py: ROLES has nine entries; six per-role tables gain hand.
  Responsibilities domain_mastery/learned_skill/specialized_task/tool_use;
  forbidden final_decision/repo_action/security_decision (v1 withholds
  repo_action — adding is compatible, removing is a break; issue #180).
  build_role_registry's hand-typed role tuple is now DERIVED from ROLES.

t3 profiles: schema ROLES, ROLE_ENV_PREFIX hand->HAND. OPT_IN_CORE_ROLES
  stays ("muse","worker") — hand is default-hosted.

t4 gateway: `hand` backend (HAND_BASE_URL), Backend.adapters, `hand:<domain>`
  aliases, and adapter honesty via a live probe of the lane's OWN /v1/models
  (probe_backend_adapters) — NOT a filesystem check, since adapter paths are
  mounted into vllm-hand, not the gateway. hand is deliberately absent from
  all three peer channels: NEVER_PROXIED_BACKENDS names that absence so a
  symmetry-minded refactor must delete a constant to break it.

t5 pressure: hand is the servable floor (_FLOOR_TIER); shed set unchanged.
t6 vllm-hand compose lane, default-on, --enable-lora armed + empty inventory.
   mg-logwrap now drops `--flag=` tokens with an explicitly empty value — a
   compose command list cannot omit an argument conditionally, and vLLM would
   parse `--lora-modules=` as a malformed name=path pair.
t7 minor/governance.py: ROLE = "hand"; duty lists re-derived for an
   adapter-dependent specialist and deliberately unchanged (competence is not
   authority). The module still names no model id — its own invariant.
t8 hand declared in all 4 card profiles (per-card util: 0.06 on the 128 GB
   Spark/Thor, 0.10 on the 64 GB Orin) and all 7 shapes; 28 goldens
   regenerated, diff is HAND_* lines only, zero deletions.
t9 roles_measure family map + dispatch (fixes the documented KeyError class),
   colleague-stack bundle, GPU_SERVICES.

Budgets are DECLARED, not measured (#108) — t10 re-derives and t12 validates.

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

Wave 1's documentation sweep for the ninth Colleague role.

Role-count phrasing: every "eight roles" / "eight-role contract" occurrence
across docs/, CLAUDE.md and README.md now reads nine. The sweep also caught
counts that were ALREADY stale before this change and had never caught up with
`worker` — lobes/explain/catalog.py said SEVEN throughout, and several
capabilities/measure/learn strings said six or seven. docs/specs/ and
docs/plans/ are deliberately untouched: they are dated historical artifacts.

New content:
  - docs/colleague-stack.md gains a `hand` section (the worker-vs-hand table,
    the three consequences of being cheap, adapter addressing) and the
    ADDING A ROLE IS EFFECTIVELY IRREVERSIBLE callout the plan asked for —
    it enumerates the surfaces a role name lands on and says what to reach
    for instead when a new role is not actually the answer.
  - CLAUDE.md gains the `hand` paragraph and updated tier vocabulary
    (capability order hand < multimodal < worker < muse < main; the floor is
    now spelled `hand`).
  - README gains the per-model doc entry.
  - docs/qwen3.5-4b-minor.md is re-headed as the DEMOTED gear, with the
    "read this in the past tense" note: its LoRA promises were never realised
    on that checkpoint, which is why the role moved.

Honesty discipline (#108): docs/machine-profiles.md now states that `hand` is
DECLARED on every card and VALIDATED on none, that this holds PER CARD so one
board's boot never promotes another, and that the Orin value specifically must
come from a real boot rather than the Spark's number.

Re-verified against the tree, as t11 requires: zero LoRA surface existed
before this change, `unsloth` appears under lobes/ only in comments and model
ids (never as a code dependency), and the before-state was eight roles.

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

MEASURED on a physical Jetson AGX Orin, 2026-08-10. Booting the lane as
committed produced:

    Available KV cache memory: -9.25 GiB

at util 0.10 — NEGATIVE, so the engine could not allocate a KV cache at all
and the boot died. vLLM's own log named the cause: its cudagraph memory
ESTIMATE, which every other lane on this nightly image already disables
(primary, embed, rerank, multimodal, multimodal-coder, muse, worker all set
VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS=0). The hand lane was the only
omission — a copy-paste gap that offline tests could not have caught, since
nothing about the rendered compose is wrong until an engine profiles it.

vLLM offered two remedies: raise util 0.10 -> 0.2762, or disable the estimate.
We disable it, matching every sibling lane. Raising util instead would make
`hand` cost 4.6x more memory on every card to buy exactly nothing.

Also adds the HAND_CUDAGRAPH_ESTIMATE escape hatch (default 0), mirroring
MULTIMODAL_/MUSE_/MULTIMODAL_CODER_CUDAGRAPH_ESTIMATE.

Also bumps to 0.56.0 with the full changelog for the hand lobe.

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

Two evidence transcripts land, and the Orin's committed budget changes as a
direct result — r1 (the blocking risk) materialised exactly as written.

docs/evidence/2026-08-10-hand-lobe-budget-derivation.txt (t10)
  Records THREE findings, two of them refutations:
  A. The lane was missing VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS=0 (fixed in
     the previous commit). With the estimate on, hand profiled to "Available
     KV cache memory: -9.25 GiB" — negative, so no boot at any util.
  B. Orin at the DECLARED 0.10: "Free memory on device cuda:0 (4.67/61.34 GiB)
     on startup is less than desired GPU memory utilization (0.1, 6.13 GiB)".
     No headroom beside the orin-lobe shape's committed lanes.
  C. Thor, with LoRA armed on a loaded box (108/122 GiB, load avg 4.30), died
     in LoRA embedding-slot allocation (vocal_parallel_embedding.py:49,
     "CUDA error: device not ready"). Deliberately left UNATTRIBUTED — three
     candidate causes are listed rather than guessed between.

docs/evidence/2026-08-10-accept-hand-orin.txt (t12) — Orin VALIDATED
  gpu_mem_util 0.06 at the full 32768 window: available KV 2.7 GiB, pool
  235,721 tokens, 7.19x concurrency, co-resident with senses + the pooling
  gears. /health 200. /v1/models lists exactly one id (no phantom adapter).
  Known-answer completion correct ("Paris"), reasoning: null.

  The check this lane exists to pass: a tool call returns a STRUCTURED
  tool_calls array — {"name":"get_weather","arguments":"{\"city\": \"Paris\"}"},
  finish_reason "tool_calls", content null — the exact opposite of the
  gemma4/`pythonic` failure mode on every field. The tokenizer's
  <|tool_call_start|>/<|tool_call_end|> were read from the actually-pulled
  revision (ids 10 and 11).

  An unknown model id 404s rather than being served under the base weights.

BUDGET CHANGE: the Orin card profile moves 0.10 -> 0.06 (measured). The
reasoning behind 0.10 — "0.06 of 64 GB leaves too little KV after the weights"
— was plausible and WRONG: 0.06 yields 7.19x concurrency there. Note the
per-card mechanism is retained but now carries no divergence; every card
declares 0.06, and only the Orin's is measured.

Per #108 the docs state VALIDATED for the Orin ONLY. Thor, Spark and base stay
DECLARED. Both probe containers were removed; every production container on
both boxes was verified still running.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PcC3i2xBuGcVPPH4tTr5pv
…+Thor runs

r1 (blocking, budget headroom) and r2 (per-card util) both resolved with
MEASURED outcomes rather than promises — r2 was REFUTED outright. Two new
risks recorded from what the live runs actually found: the missing
cudagraph-estimate knob (r5, follow_up — an offline compose env-parity check
would have caught it and does not exist) and Thor's unattributed LoRA
embedding-slot failure (r6, unknown_nonblocking).

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

Every gap the live runs exposed now points at its tracking issue from the
place a reader hits it: the missing cudagraph parity test (#182) from the
lane comment and the derivation transcript, Thor's unattributed LoRA failure
(#181) from the derivation transcript and the validation table, and the
remaining Thor/Spark/adapter validation (#183) from the acceptance transcript
and the per-model doc.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PcC3i2xBuGcVPPH4tTr5pv
Its 'What is NOT claimed' section was written before the corrected Orin boot
succeeded, so it still said NO card is validated and the tool-call round trip
was not exercised. Both became false minutes later. A transcript that
understates is less harmful than one that overstates, but it directly
contradicted the acceptance transcript sitting beside it.

Scoped rather than rewritten: the file now says up front that it covers the
DERIVATION runs only, and each item in the list distinguishes what THESE runs
did not establish from what the later run settled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PcC3i2xBuGcVPPH4tTr5pv
…d by nothing

Second live finding of the same class as the cudagraph one, caught by booting
the ACTUAL committed compose lane rather than a hand-rolled docker run.

builtin/orin.toml declares attention_backend = "TRITON_ATTN" for hand, and
`lobes init --profile orin` duly renders HAND_ATTENTION_BACKEND=TRITON_ATTN
into the deployment's .env. The vllm-hand lane never substituted it. The
operator reads a configured attention backend; the engine never sees one.

That is precisely the half-honest posture #92 exists to forbid, and it is why
my earlier manual probe and the compose lane disagreed: the probe set the
backend by hand, the lane could not.

Fixed via --attention-config (NOT VLLM_ATTENTION_BACKEND, which is gone on this
nightly — the vllm-embed lane already documents that), defaulting to "auto",
which vLLM treats as its automatic-selection sentinel and is byte-equivalent to
omitting the flag on cards that declare nothing.

Adds a test for the whole CLASS, not just this instance:
test_every_rendered_profile_knob_is_substituted_by_the_fleet_template asserts
that every KEY any card profile renders appears as ${KEY} in the fleet
template. Verified it FAILS with the fix reverted, so it genuinely catches the
bug it was written for rather than merely passing alongside it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PcC3i2xBuGcVPPH4tTr5pv
I wrote an acceptance transcript after ONE successful boot. Two later boots of
the same configuration on the same box refused to start. That is not a
validated budget and the claim is withdrawn before this branch becomes a PR.

Three boots at identical gpu_mem_util=0.06 / max_model_len=32768:
    run 1 (docker run)     -> Available KV 2.7  GiB -> served
    run 2 (compose)        -> Available KV 0.14 GiB -> ValueError
    run 3 (compose, clean) -> Available KV 0.09 GiB -> ValueError, "estimated
                                                       maximum model length 7984"

vLLM clamps its budget against actual free memory at startup. The Orin is
shared — senses at 0.45, the pooling gears, unrelated production containers —
and its free memory moved ~2.7 GiB across these runs. A util leaving ~1 GiB of
margin on a 61 GiB card sits inside that noise.

WHAT SURVIVES, because it was observed on a real serving engine and does not
depend on the budget: the lfm2 parser, the structured tool_calls array, the
tokenizer delimiters (ids 10/11), the bf16 sentinel, the unknown-id 404.

WHAT DOES NOT: "Orin VALIDATED", and 0.06/235,721 tokens/7.19x as a committed
measurement. NO card is validated for hand.

Changes: the transcript is renamed ...-accept-... -> ...-partial-... and
carries the retraction at its head; orin.toml relabels 0.06 as one data point
with operator guidance for a box that refuses it; the per-model doc,
machine-profiles.md, the delivery summary and CHANGELOG all read DECLARED.

Recorded as deviation d9. The Orin joins #183.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PcC3i2xBuGcVPPH4tTr5pv
…et reproduces (0.56.1)

The Spark was the one card `hand` had never been exercised on. It serves, and
every functional probe passes on this third card: `Lfm2ForCausalLM` resolved,
`quantization=None` (the bf16 sentinel), `reasoning_parser=''`,
`PunicaWrapperGPU` (LoRA armed on an empty inventory), a correct known-answer,
a structured `tool_calls` array with `finish_reason: "tool_calls"` — the
gemma4 prose-relay trap not happening — an undeclared `hand:<domain>` refused
404, and `GET /v1/models` carrying exactly one entry, the base. Two results are
new here: the first live confirmation that the d8 `--attention-config` fix
actually resolves (rather than merely not crashing), and the committed lane
reaching compose `healthy`.

The budget still does not reproduce, and that is the finding worth keeping.
Three boots minutes apart at the identical `gpu_mem_util = 0.06` profiled
6.21 / 3.34 / 3.54 GiB of available KV. Runs 2 and 3 agree within 6% and were
taken at the same free-memory level; run 1 followed 31.7 GiB being freed. So
the pool tracks memory free AT THAT INSTANT, not the util:

  On a unified-memory card with co-resident tenants, gpu_mem_util does not name
  a stable budget. A single boot's KV number measures the box's state, not the
  card's capacity for the role.

That retro-explains the Orin retraction as a property rather than a fluke, and
it is not a tight-margin artifact — the Orin showed it with ~1 GiB of margin on
a 61 GiB card, the Spark with several GiB on a 121 GiB one. All four cards stay
DECLARED (#183); no pool or concurrency figure is promoted anywhere.

Also recorded: the four earlier Spark boot failures were memory exhaustion on a
shared workstation (swap 100% full, 16 GiB available, a browser holding
31.7 GiB), not a defect in the lane. With that freed, the identical
`docker compose up -d` returned in 10s instead of 10+ minutes and the engine
served in 71.75s. And `--lora-modules=` is absent from the post-strip argv on a
third machine, so deviation d3 is three-for-three across Thor/Orin/Spark.

One drive-by: tests/test_shape_goldens.py was committed in a state black would
reformat, which CI's lint job would have failed on. Formatted.

Recorded as deviation d10 against t12.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PcC3i2xBuGcVPPH4tTr5pv
Four sections move: t12's row (Orin + Spark, neither budget reproducing), the
mid-work decisions (d10), the claims table (Spark functional now high, "any
card has a reproducible budget" demoted from unverified to REFUTED with the
mechanism named), and remaining work (the Spark half of #183 is functionally
done; a per-card budget may not be the right thing to chase at all).

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

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add hand role (LFM2.5-1.2B) as cheap tier + LoRA base, never-proxied floor

✨ Enhancement ⚙️ Configuration changes 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add hand as ninth Colleague role, default-hosted everywhere.
• Repoint minor/cheap to hand; make hand the pressure-policy floor.
• Add LoRA adapter wiring and engine-verified adapter advertising.
Diagram

graph TD
  A["Client / Colleague"] --> B["Gateway server"] --> C["Role registry (lobes/roles.py)"] --> D["RoutingTable (lobes/gateway/_routing.py)"] --> H["vllm-hand (LFM2.5 + LoRA)"]
  B --> E["Config builder (lobes/gateway/_config.py)"] --> D
  B --> F["ReadinessCache (lobes/gateway/_readiness.py)"] --> D
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Model each adapter as its own backend/service
  • ➕ Keeps routing semantics to one served_name per backend.
  • ➕ Allows per-adapter resource isolation.
  • ➖ Operational explosion (one container per adapter).
  • ➖ Undermines the intended “one base, many adapters” muscle-memory model.
2. Support runtime hot-load/unload of adapters
  • ➕ No lane restart to add/remove adapters.
  • ➕ Easier iterative experimentation.
  • ➖ Introduces mutable serving state and new failure modes.
  • ➖ Requires additional APIs and synchronization across gateway/engine.
3. Allow peer proxying for `hand` like other roles
  • ➕ More uniform peer-channel logic.
  • ➕ Fewer special cases in config code.
  • ➖ Violates the core invariant that hand is always local and adapters are local-mount dependent.
  • ➖ Risks misleading behavior when peers lack adapter files.

Recommendation: Keep the PR’s approach: hand is a first-class role with a default-on lane, adapters are additional served ids on that same backend, and adapter advertisement is gated by engine evidence. The explicit NEVER_PROXIED_BACKENDS constant is a good guardrail against symmetry-driven refactors that would break the intended invariant.

Files changed (126) +5639 / -412

Enhancement (6) +480 / -42
catalog.pyAdd LFM2.5 model and repoint tier aliases to 'hand' +71/-14

Add LFM2.5 model and repoint tier aliases to 'hand'

• Introduces 'LiquidAI/LFM2.5-1.2B-Instruct' as 'role_hint="hand"' and updates 'minor'/'cheap' tier aliases to resolve to the hand backend, demoting Qwen3.5-4B to a 'candidate' entry.

lobes/catalog.py

_config.pyAdd hand backend config + adapter aliasing + never-proxied constant +149/-5

Add hand backend config + adapter aliasing + never-proxied constant

• Adds '_DEFAULT_HAND', a hand feasibility knob, parses 'HAND_LORA_MODULES', introduces 'NEVER_PROXIED_BACKENDS={'hand'}', and generates 'hand:<domain>' aliases only when hand is wired.

lobes/gateway/_config.py

_readiness.pyProbe loaded LoRA adapters via backend /v1/models +114/-0

Probe loaded LoRA adapters via backend /v1/models

• Adds 'probe_backend_adapters' and extends 'ReadinessCache' to maintain a backend→loaded-adapters map, fail-closed to empty on probe errors.

lobes/gateway/_readiness.py

_routing.pyRoute and advertise adapter model ids +50/-3

Route and advertise adapter model ids

• Extends 'Backend' with 'adapters', treats adapter ids as known for routing/404 decisions, and lists adapters only when confirmed loaded by readiness probes.

lobes/gateway/_routing.py

server.pyWire adapter readiness into /v1/models and expand role contract prose +30/-5

Wire adapter readiness into /v1/models and expand role contract prose

• Passes 'ReadinessCache.current_adapters()' into '/v1/models' payload generation so only engine-confirmed adapters are advertised.

lobes/gateway/server.py

roles.pyAdd 'hand' as ninth role and derive gateway-fronted roles +66/-15

Add 'hand' as ninth role and derive gateway-fronted roles

• Introduces 'hand' role metadata/responsibilities/forbidden set, updates role-to-backend mappings, and derives 'GATEWAY_FRONTED_ROLES' from 'ROLES' to prevent half-landed roles.

lobes/roles.py

Bug fix (6) +161 / -34
route.pyRoute classifier now uses hand (minor back-compat retained) +18/-9

Route classifier now uses hand (minor back-compat retained)

• Switches routing-classifier model selection from 'role_hint=minor' to 'role_hint=hand' while still honoring 'minor' in model outputs as a synonym.

lobes/cli/_commands/route.py

_pressure_policy.pyMake 'hand' the pressure-policy servable floor +24/-11

Make 'hand' the pressure-policy servable floor

• Normalizes 'minor'/'cheap' to 'hand' and uses '_FLOOR_TIER='hand'' so shedding and reported servable_tier stay consistent.

lobes/gateway/_pressure_policy.py

governance.pyGovern cheap-tier under ROLE='hand' (module path retained) +61/-12

Govern cheap-tier under ROLE='hand' (module path retained)

• Updates governance policy to apply to the 'hand' role while keeping the 'lobes.minor' module path for compatibility and clarifying why governance is adapter-independent.

lobes/minor/governance.py

roles_measure.pyInclude hand in measurement role families +6/-2

Include hand in measurement role families

• Adds 'hand' to LLM role family and measurement dispatch maps to ensure 'lobes measure' covers all roles without KeyError.

lobes/roles_measure.py

_parser.pyAuto-detect LFM2 tool parser key as 'lfm2' +25/-0

Auto-detect LFM2 tool parser key as 'lfm2'

• Extends tool parser inference to recognize LFM2/LFM2.5 model ids and select vLLM’s 'lfm2' parser to avoid silent tool-call parsing failures.

lobes/runtime/_parser.py

mg-logwrap.shDrop '--flag=' argv entries to support empty templated values +27/-0

Drop '--flag=' argv entries to support empty templated values

• Filters out explicitly-empty long flags like '--lora-modules=' so static compose command lists can template optional values safely.

lobes/templates/mg-logwrap.sh

Tests (60) +722 / -185
base.envUpdate golden env for new HAND_* defaults +4/-0

Update golden env for new HAND_* defaults

• Updates rendered env golden to include HAND_* keys introduced by the hand lane.

tests/goldens/base.env

orin.envUpdate Orin golden env for hand +5/-0

Update Orin golden env for hand

• Updates Orin golden env output to include HAND_* values rendered from the Orin profile.

tests/goldens/orin.env

regen.pyAdjust golden regeneration for new role and template behavior +25/-11

Adjust golden regeneration for new role and template behavior

• Updates regeneration logic to handle the expanded role set and template changes affecting env output.

tests/goldens/regen.py

orin-lobe__base.envUpdate shape golden env (orin-lobe × base) for hand +4/-0

Update shape golden env (orin-lobe × base) for hand

• Refreshes shape×card golden env to include HAND_* keys where applicable.

tests/goldens/shapes/orin-lobe__base.env

orin-lobe__orin.envUpdate shape golden env (orin-lobe × orin) for hand +5/-0

Update shape golden env (orin-lobe × orin) for hand

• Refreshes shape×card golden env to include HAND_* keys where applicable.

tests/goldens/shapes/orin-lobe__orin.env

orin-lobe__spark.envUpdate shape golden env (orin-lobe × spark) for hand +4/-0

Update shape golden env (orin-lobe × spark) for hand

• Refreshes shape×card golden env to include HAND_* keys where applicable.

tests/goldens/shapes/orin-lobe__spark.env

orin-lobe__thor.envUpdate shape golden env (orin-lobe × thor) for hand +4/-0

Update shape golden env (orin-lobe × thor) for hand

• Refreshes shape×card golden env to include HAND_* keys where applicable.

tests/goldens/shapes/orin-lobe__thor.env

orin-small__base.envUpdate shape golden env (orin-small × base) for hand +4/-0

Update shape golden env (orin-small × base) for hand

• Refreshes shape×card golden env to include HAND_* keys where applicable.

tests/goldens/shapes/orin-small__base.env

orin-small__orin.envUpdate shape golden env (orin-small × orin) for hand +5/-0

Update shape golden env (orin-small × orin) for hand

• Refreshes shape×card golden env to include HAND_* keys where applicable.

tests/goldens/shapes/orin-small__orin.env

orin-small__spark.envUpdate shape golden env (orin-small × spark) for hand +4/-0

Update shape golden env (orin-small × spark) for hand

• Refreshes shape×card golden env to include HAND_* keys where applicable.

tests/goldens/shapes/orin-small__spark.env

orin-small__thor.envUpdate shape golden env (orin-small × thor) for hand +4/-0

Update shape golden env (orin-small × thor) for hand

• Refreshes shape×card golden env to include HAND_* keys where applicable.

tests/goldens/shapes/orin-small__thor.env

spark-lobe__base.envUpdate shape golden env (spark-lobe × base) for hand +4/-0

Update shape golden env (spark-lobe × base) for hand

• Refreshes shape×card golden env to include HAND_* keys where applicable.

tests/goldens/shapes/spark-lobe__base.env

spark-lobe__orin.envUpdate shape golden env (spark-lobe × orin) for hand +5/-0

Update shape golden env (spark-lobe × orin) for hand

• Refreshes shape×card golden env to include HAND_* keys where applicable.

tests/goldens/shapes/spark-lobe__orin.env

spark-lobe__spark.envUpdate shape golden env (spark-lobe × spark) for hand +4/-0

Update shape golden env (spark-lobe × spark) for hand

• Refreshes shape×card golden env to include HAND_* keys where applicable.

tests/goldens/shapes/spark-lobe__spark.env

spark-lobe__thor.envUpdate shape golden env (spark-lobe × thor) for hand +4/-0

Update shape golden env (spark-lobe × thor) for hand

• Refreshes shape×card golden env to include HAND_* keys where applicable.

tests/goldens/shapes/spark-lobe__thor.env

thor-lobe__base.envUpdate shape golden env (thor-lobe × base) for hand +4/-0

Update shape golden env (thor-lobe × base) for hand

• Refreshes shape×card golden env to include HAND_* keys where applicable.

tests/goldens/shapes/thor-lobe__base.env

thor-lobe__orin.envUpdate shape golden env (thor-lobe × orin) for hand +5/-0

Update shape golden env (thor-lobe × orin) for hand

• Refreshes shape×card golden env to include HAND_* keys where applicable.

tests/goldens/shapes/thor-lobe__orin.env

thor-lobe__spark.envUpdate shape golden env (thor-lobe × spark) for hand +4/-0

Update shape golden env (thor-lobe × spark) for hand

• Refreshes shape×card golden env to include HAND_* keys where applicable.

tests/goldens/shapes/thor-lobe__spark.env

thor-lobe__thor.envUpdate shape golden env (thor-lobe × thor) for hand +4/-0

Update shape golden env (thor-lobe × thor) for hand

• Refreshes shape×card golden env to include HAND_* keys where applicable.

tests/goldens/shapes/thor-lobe__thor.env

thor-muse__base.envUpdate shape golden env (thor-muse × base) for hand +4/-0

Update shape golden env (thor-muse × base) for hand

• Refreshes shape×card golden env to include HAND_* keys where applicable.

tests/goldens/shapes/thor-muse__base.env

thor-muse__orin.envUpdate shape golden env (thor-muse × orin) for hand +5/-0

Update shape golden env (thor-muse × orin) for hand

• Refreshes shape×card golden env to include HAND_* keys where applicable.

tests/goldens/shapes/thor-muse__orin.env

thor-muse__spark.envUpdate shape golden env (thor-muse × spark) for hand +4/-0

Update shape golden env (thor-muse × spark) for hand

• Refreshes shape×card golden env to include HAND_* keys where applicable.

tests/goldens/shapes/thor-muse__spark.env

thor-muse__thor.envUpdate shape golden env (thor-muse × thor) for hand +4/-0

Update shape golden env (thor-muse × thor) for hand

• Refreshes shape×card golden env to include HAND_* keys where applicable.

tests/goldens/shapes/thor-muse__thor.env

thor-worker__base.envUpdate shape golden env (thor-worker × base) for hand +4/-0

Update shape golden env (thor-worker × base) for hand

• Refreshes shape×card golden env to include HAND_* keys where applicable.

tests/goldens/shapes/thor-worker__base.env

thor-worker__orin.envUpdate shape golden env (thor-worker × orin) for hand +5/-0

Update shape golden env (thor-worker × orin) for hand

• Refreshes shape×card golden env to include HAND_* keys where applicable.

tests/goldens/shapes/thor-worker__orin.env

thor-worker__spark.envUpdate shape golden env (thor-worker × spark) for hand +4/-0

Update shape golden env (thor-worker × spark) for hand

• Refreshes shape×card golden env to include HAND_* keys where applicable.

tests/goldens/shapes/thor-worker__spark.env

thor-worker__thor.envUpdate shape golden env (thor-worker × thor) for hand +4/-0

Update shape golden env (thor-worker × thor) for hand

• Refreshes shape×card golden env to include HAND_* keys where applicable.

tests/goldens/shapes/thor-worker__thor.env

spark.envUpdate Spark golden env for hand +4/-0

Update Spark golden env for hand

• Updates Spark golden env output to include HAND_* values rendered from the Spark profile.

tests/goldens/spark.env

template-defaults.envUpdate template defaults golden for HAND_* vars +12/-0

Update template defaults golden for HAND_* vars

• Updates template defaults golden to include new environment variables and defaults introduced by the hand lane.

tests/goldens/template-defaults.env

thor.envUpdate Thor golden env for hand +4/-0

Update Thor golden env for hand

• Updates Thor golden env output to include HAND_* values rendered from the Thor profile.

tests/goldens/thor.env

test_catalog.pyUpdate catalog tests for new hand model and tier behavior +76/-9

Update catalog tests for new hand model and tier behavior

• Extends catalog assertions to include the new hand model entry and validate updated tier/candidate expectations.

tests/test_catalog.py

test_catalog_tiers.pyAssert cheap/minor tiers now resolve to hand +16/-7

Assert cheap/minor tiers now resolve to hand

• Updates tier tests so 'cheap'/'minor' resolve to the hand gear and validates new capability ordering.

tests/test_catalog_tiers.py

test_cli_capabilities.pyUpdate CLI capabilities test expectations for hand +6/-4

Update CLI capabilities test expectations for hand

• Adjusts CLI capabilities test output expectations to include the new 'hand' role.

tests/test_cli_capabilities.py

test_cli_measure.pyUpdate CLI measure tests for expanded role set +2/-1

Update CLI measure tests for expanded role set

• Updates measure-related tests to reflect nine roles including 'hand'.

tests/test_cli_measure.py

test_cli_run.pyUpdate CLI run tests for tier/role changes +7/-2

Update CLI run tests for tier/role changes

• Aligns run command tests with updated tier resolution and 'hand' role vocabulary.

tests/test_cli_run.py

test_cli_up.pyUpdate CLI up tests for colleague-stack including hand +25/-14

Update CLI up tests for colleague-stack including hand

• Updates 'lobes up' tests so colleague-stack includes 'hand' and excludes opt-in 'muse'/'worker'.

tests/test_cli_up.py

test_dropped_lobe_honesty.pyAdjust dropped-lobe honesty tests for hand semantics +2/-0

Adjust dropped-lobe honesty tests for hand semantics

• Updates expectations so 'hand' remains consistent with default-hosted, never-proxied semantics under role dropping.

tests/test_dropped_lobe_honesty.py

test_gateway_busy_e2e.pyUpdate busy-mode E2E to reflect hand floor behavior +10/-9

Update busy-mode E2E to reflect hand floor behavior

• Adjusts E2E tests so pressure shedding keeps the 'hand' tier servable while others shed under load.

tests/test_gateway_busy_e2e.py

test_gateway_config_proxy.pyAssert never-proxied backends are excluded from peer channel dicts +12/-3

Assert never-proxied backends are excluded from peer channel dicts

• Adds 'NEVER_PROXIED_BACKENDS' and asserts peer proxy/key vocabularies are FEASIBLE_ENV minus that set.

tests/test_gateway_config_proxy.py

test_gateway_feasibility.pyUpdate feasibility tests for hand backend semantics +3/-0

Update feasibility tests for hand backend semantics

• Updates feasibility expectations to treat unwired hand as feasible (sleeping lobe), not infeasible.

tests/test_gateway_feasibility.py

test_gateway_proxy.pyUpdate proxy tests for new peer-channel invariants +4/-1

Update proxy tests for new peer-channel invariants

• Adjusts proxy tests to match the new peer-channel derivation rules and hand’s deliberate absence.

tests/test_gateway_proxy.py

test_gateway_server.pyUpdate gateway server tests for adapter listing behavior +13/-12

Update gateway server tests for adapter listing behavior

• Extends server tests to validate /v1/models includes only engine-confirmed loaded adapters.

tests/test_gateway_server.py

test_gateway_tiers.pyUpdate gateway tier resolution tests: minor/cheap → hand +26/-26

Update gateway tier resolution tests: minor/cheap → hand

• Adjusts tier alias resolution tests to confirm that minor/cheap resolve to the hand backend and track custom served names.

tests/test_gateway_tiers.py

test_init_shape.pyUpdate init shape tests for hand in default-hosted roles +8/-1

Update init shape tests for hand in default-hosted roles

• Updates shape initialization tests to include hand in generated configs for built-in shapes.

tests/test_init_shape.py

test_minor_governance.pyUpdate governance tests for ROLE='hand' +3/-1

Update governance tests for ROLE='hand'

• Updates tests to ensure governance is role-keyed and now applies to 'hand' while remaining model-id agnostic.

tests/test_minor_governance.py

test_minor_integration.pyUpdate integration tests for minor/hand tier changes +26/-8

Update integration tests for minor/hand tier changes

• Adjusts integration expectations where 'minor'/'cheap' now resolve to 'hand' and governance applies to the cheap tier role.

tests/test_minor_integration.py

test_parser.pyUpdate tool parser detection tests for lfm2 +12/-0

Update tool parser detection tests for lfm2

• Extends parser tests to validate 'lfm2' selection for LiquidAI LFM2/LFM2.5 model ids.

tests/test_parser.py

test_peer_referral.pyUpdate peer referral tests for hand never-proxied invariant +9/-2

Update peer referral tests for hand never-proxied invariant

• Updates peer referral expectations so 'hand' is excluded from peer referral/proxy channels.

tests/test_peer_referral.py

test_pressure_policy.pyUpdate pressure policy tests: servable_tier becomes hand +25/-22

Update pressure policy tests: servable_tier becomes hand

• Rewrites assertions so the floor tier is 'hand' and verifies normalized outputs never emit legacy 'minor'/'cheap'.

tests/test_pressure_policy.py

test_profile_render.pyUpdate profile render tests for hand knobs +1/-0

Update profile render tests for hand knobs

• Updates profile render tests to account for 'hand' role keys rendered into env output.

tests/test_profile_render.py

test_profile_schema.pyUpdate schema tests for hand role inclusion +1/-1

Update schema tests for hand role inclusion

• Adjusts schema tests to validate 'hand' is part of the profile role schema.

tests/test_profile_schema.py

test_proxy_integration.pyUpdate proxy integration tests for new peer-channel derivations +35/-0

Update proxy integration tests for new peer-channel derivations

• Updates integration proxy behavior tests to match never-proxied backends handling and role vocabulary.

tests/test_proxy_integration.py

test_roles.pyExpand role registry tests to nine roles and add completeness guards +116/-9

Expand role registry tests to nine roles and add completeness guards

• Updates role tests to include 'hand' and adds parametric guards ensuring all per-role tables cover ROLES, preventing half-landed roles.

tests/test_roles.py

test_shape_contract_matrix.pyUpdate shape contract matrix for hand default hosting +14/-4

Update shape contract matrix for hand default hosting

• Updates shape/contract matrix tests to include 'hand' and validate hosting expectations across shapes.

tests/test_shape_contract_matrix.py

test_shape_goldens.pyUpdate golden shape expectations for hand additions +33/-0

Update golden shape expectations for hand additions

• Updates shape golden tests to ensure regenerated env goldens include 'hand'-related keys consistently.

tests/test_shape_goldens.py

test_shapes.pyUpdate shapes tests for nine-role contract and hand hosting +19/-6

Update shapes tests for nine-role contract and hand hosting

• Adjusts shape behavior tests to include 'hand' in the default-hosted role set and validate shape semantics accordingly.

tests/test_shapes.py

test_smoke_duo.pyUpdate smoke tests for tier and role vocabulary changes +17/-15

Update smoke tests for tier and role vocabulary changes

• Updates smoke tests to reflect new tier normalization and the presence of 'hand' as the cheap tier.

tests/test_smoke_duo.py

test_status_pressure.pyUpdate status/pressure tests for hand floor semantics +9/-6

Update status/pressure tests for hand floor semantics

• Adjusts status pressure tests to ensure pressure-mode reporting reflects 'hand' as the floor tier.

tests/test_status_pressure.py

test_tier_request.pyUpdate tier request tests for minor/cheap → hand normalization +12/-8

Update tier request tests for minor/cheap → hand normalization

• Updates tier request handling tests to validate normalization and behavior when callers request legacy tiers.

tests/test_tier_request.py

test_tool_parser_plugin.pyUpdate compose-hash tripwire for new vllm-hand service +24/-3

Update compose-hash tripwire for new vllm-hand service

• Recomputes expected hashes to account for the newly added 'vllm-hand' service and gateway HAND_* passthrough changes.

tests/test_tool_parser_plugin.py

Documentation (33) +3895 / -125
currentPoint devague to the hand-lobe frame +1/-1

Point devague to the hand-lobe frame

• Updates the devague current pointer to the new hand-lobe frame artifact.

.devague/current

current_planPoint devague to the hand-lobe plan +1/-1

Point devague to the hand-lobe plan

• Updates the devague current_plan pointer to the new hand-lobe plan artifact.

.devague/current_plan

hand-lobe-lfm2-5-1-2b.jsonAdd hand-lobe delivery record +135/-0

Add hand-lobe delivery record

• Adds the delivery JSON capturing the finalized delivery summary and acceptance notes for the hand lobe.

.devague/deliveries/hand-lobe-lfm2-5-1-2b.json

hand-lobe-lfm2-5-1-2b.jsonAdd hand-lobe announcement/spec frame +1151/-0

Add hand-lobe announcement/spec frame

• Adds the full devague frame content for the hand-lobe work, including decision rationale and reviewer-checkable invariants.

.devague/frames/hand-lobe-lfm2-5-1-2b.json

hand-lobe-lfm2-5-1-2b.jsonAdd hand-lobe implementation plan +884/-0

Add hand-lobe implementation plan

• Adds the full devague plan JSON for implementing the hand role across catalog, gateway, templates, and tests.

.devague/plans/hand-lobe-lfm2-5-1-2b.json

lobes__public.jsonlUpdate public memory index for new hand artifacts +5/-5

Update public memory index for new hand artifacts

• Refreshes the eidetic memory JSONL to include/adjust entries related to the new hand lobe and public docs.

.eidetic/memory/lobes__public.jsonl

CHANGELOG.mdDocument 0.56.0/0.56.1: hand role, LoRA serving, and fixes +36/-0

Document 0.56.0/0.56.1: hand role, LoRA serving, and fixes

• Adds detailed changelog entries describing the new 'hand' role, tier repointing, LoRA adapter wiring, parser selection, and related fixes/validation notes.

CHANGELOG.md

CLAUDE.mdUpdate Colleague role contract docs to include hand +85/-20

Update Colleague role contract docs to include hand

• Expands role-count and contract guidance to reflect nine roles and the new 'hand' responsibilities/constraints.

CLAUDE.md

README.mdUpdate README for hand role, tiers, and adapter addressing +20/-9

Update README for hand role, tiers, and adapter addressing

• Updates fleet overview to include 'hand', explain tier repointing, and document 'hand:<domain>' adapter naming.

README.md

colleague-stack.mdAdd 'hand' to the nine-role Colleague contract +92/-19

Add 'hand' to the nine-role Colleague contract

• Updates the contract reference to include 'hand' and adds explicit guidance that adding roles is effectively irreversible.

docs/colleague-stack.md

2026-08-10-hand-lobe-lfm2-5-1-2b.mdAdd hand-lobe delivery writeup +181/-0

Add hand-lobe delivery writeup

• Adds the human-readable delivery document for the hand lobe, including acceptance/validation status and follow-ups.

docs/deliveries/2026-08-10-hand-lobe-lfm2-5-1-2b.md

deployment-shapes.mdRefresh deployment-shape documentation for hand hosting +5/-5

Refresh deployment-shape documentation for hand hosting

• Updates shape documentation to reflect 'hand' as default-hosted and aligns role counts/hosting rules.

docs/deployment-shapes.md

2026-08-10-hand-lobe-budget-derivation.txtAdd evidence: hand budget derivation notes +148/-0

Add evidence: hand budget derivation notes

• Adds evidence log describing memory-budget derivation and observed variability for the hand lane across hardware.

docs/evidence/2026-08-10-hand-lobe-budget-derivation.txt

2026-08-10-partial-hand-orin.txtAdd evidence: partial Orin hand run transcript +181/-0

Add evidence: partial Orin hand run transcript

• Adds Orin transcript documenting functional checks and budget non-reproducibility observations for the hand lane.

docs/evidence/2026-08-10-partial-hand-orin.txt

2026-08-10-partial-hand-spark.txtAdd evidence: partial Spark hand run transcript +189/-0

Add evidence: partial Spark hand run transcript

• Adds Spark transcript confirming hand functional behavior, tool_calls structure, and configuration fixes.

docs/evidence/2026-08-10-partial-hand-spark.txt

gateway-fleet.mdUpdate gateway fleet doc for hand lane and tier semantics +4/-3

Update gateway fleet doc for hand lane and tier semantics

• Adjusts fleet topology documentation to include the new default-on hand lane and updated tier behavior.

docs/gateway-fleet.md

gemma-4-31b-nvfp4.mdAlign gemma4 doc with updated vocabulary +1/-1

Align gemma4 doc with updated vocabulary

• Small documentation alignment for role/tier vocabulary consistency.

docs/gemma-4-31b-nvfp4.md

lfm2.5-1.2b-hand.mdAdd per-model reference for LFM2.5 hand lane +269/-0

Add per-model reference for LFM2.5 hand lane

• Adds a dedicated model doc describing LFM2.5-1.2B properties, required vLLM/tool parser settings, and deliberate flag omissions.

docs/lfm2.5-1.2b-hand.md

machine-profiles.mdUpdate machine profile docs for hand knobs +41/-6

Update machine profile docs for hand knobs

• Updates machine profile documentation to include hand role tuning keys and clarify per-card budgeting considerations.

docs/machine-profiles.md

openai-api.mdUpdate API docs for hand role and model ids +6/-6

Update API docs for hand role and model ids

• Aligns OpenAI API documentation with the new role count and 'hand'/adapter model naming behavior.

docs/openai-api.md

2026-08-10-hand-lobe-lfm2-5-1-2b.mdAdd hand-lobe plan markdown +133/-0

Add hand-lobe plan markdown

• Adds the human-readable plan document describing tasks and acceptance criteria for hand role integration.

docs/plans/2026-08-10-hand-lobe-lfm2-5-1-2b.md

qwen3-embedding-4b.mdAlign embedding doc with updated vocabulary +1/-1

Align embedding doc with updated vocabulary

• Small documentation alignment for role/tier vocabulary consistency.

docs/qwen3-embedding-4b.md

qwen3.5-4b-minor.mdReframe Qwen3.5-4B as demoted candidate gear +21/-4

Reframe Qwen3.5-4B as demoted candidate gear

• Updates doc to reflect Qwen3.5-4B is no longer the 'minor'/'cheap' tier target but remains selectable as a candidate.

docs/qwen3.5-4b-minor.md

qwen3.6-35b-a3b-nvfp4.mdAlign worker doc with updated vocabulary +3/-3

Align worker doc with updated vocabulary

• Small documentation alignment for role/hosting vocabulary consistency.

docs/qwen3.6-35b-a3b-nvfp4.md

2026-08-09-hand-lobe-lfm2-5-1-2b.mdAdd hand-lobe spec markdown +174/-0

Add hand-lobe spec markdown

• Adds the hand-lobe spec defining responsibilities, hosting invariants, and LFM2.5 serving choices.

docs/specs/2026-08-09-hand-lobe-lfm2-5-1-2b.md

capabilities.pyUpdate capabilities CLI output for nine roles +8/-8

Update capabilities CLI output for nine roles

• Adjusts CLI capabilities reporting strings to reflect nine roles including 'hand'.

lobes/cli/_commands/capabilities.py

init.pyUpdate init messaging for hand defaults +1/-1

Update init messaging for hand defaults

• Updates init messaging/help text to align with the new default-hosted role set including 'hand'.

lobes/cli/_commands/init.py

learn.pyUpdate learn command wording for hand/tier changes +1/-1

Update learn command wording for hand/tier changes

• Updates help/output text to reflect role count and tier naming changes introduced by 'hand'.

lobes/cli/_commands/learn.py

measure.pyUpdate measure command wording for nine roles +1/-1

Update measure command wording for nine roles

• Aligns 'lobes measure' messaging with the expanded role set including 'hand'.

lobes/cli/_commands/measure.py

overview.pyUpdate overview command wording for hand and tiers +1/-1

Update overview command wording for hand and tiers

• Refreshes overview output/help text to reflect current role/tier vocabulary after introducing 'hand'.

lobes/cli/_commands/overview.py

run.pyUpdate run command messaging for hand role/tier semantics +10/-5

Update run command messaging for hand role/tier semantics

• Adjusts CLI run help/output strings to align with the new role count and tier naming.

lobes/cli/_commands/run.py

catalog.pyUpdate explain docs for nine-role contract and hand knobs +42/-24

Update explain docs for nine-role contract and hand knobs

• Refreshes 'lobes explain roles' and related sections to include 'hand', update tier ordering, and align endpoint/contract descriptions.

lobes/explain/catalog.py

env.exampleDocument hand env knobs and LoRA adapter inventory +64/-0

Document hand env knobs and LoRA adapter inventory

• Adds HAND_* environment variable documentation including adapter inventory semantics, deliberate peer-channel absence, and operational caveats.

lobes/templates/fleet/env.example

Other (21) +381 / -26
__init__.pyAlign CLI module metadata for new release +1/-1

Align CLI module metadata for new release

• Updates CLI module-level metadata to stay consistent with the release that introduces the 'hand' role.

lobes/cli/init.py

up.pyUpdate colleague-stack defaults to include hand +11/-7

Update colleague-stack defaults to include hand

• Updates 'lobes up colleague-stack' docs/behavior to include 'hand' and keep 'muse'/'worker' excluded as opt-in-hosted roles.

lobes/cli/_commands/up.py

base.tomlAdd '[roles.hand]' defaults for unknown hardware +21/-0

Add '[roles.hand]' defaults for unknown hardware

• Adds hand tuning defaults (feasible/model/gpu_mem_util/max_model_len) and documents deliberate absence of quantization flags.

lobes/profiles/builtin/base.toml

orin.tomlAdd Orin '[roles.hand]' tuning and caveats +46/-0

Add Orin '[roles.hand]' tuning and caveats

• Adds hand tuning block for Orin including attention backend and extensive notes about declared budgets and observed variability.

lobes/profiles/builtin/orin.toml

spark.tomlAdd Spark '[roles.hand]' tuning +29/-0

Add Spark '[roles.hand]' tuning

• Adds hand tuning block for Spark to ensure the hand lane is rendered by default on that card profile.

lobes/profiles/builtin/spark.toml

thor.tomlAdd Thor '[roles.hand]' tuning +20/-0

Add Thor '[roles.hand]' tuning

• Adds hand tuning block for Thor to ensure the hand lane is rendered by default on that card profile.

lobes/profiles/builtin/thor.toml

machine-as-brain.tomlInclude hand in machine-as-brain hosts +10/-2

Include hand in machine-as-brain hosts

• Updates default shape hosting list to include 'hand' as default-hosted everywhere.

lobes/profiles/builtin_shapes/machine-as-brain.toml

orin-lobe.tomlInclude hand in orin-lobe hosts +9/-1

Include hand in orin-lobe hosts

• Ensures 'hand' remains hosted in the Orin mesh-lobe shape.

lobes/profiles/builtin_shapes/orin-lobe.toml

orin-small.tomlInclude hand in orin-small hosts +10/-2

Include hand in orin-small hosts

• Ensures 'hand' remains hosted in the Orin small shape.

lobes/profiles/builtin_shapes/orin-small.toml

spark-lobe.tomlInclude hand in spark-lobe hosts +9/-1

Include hand in spark-lobe hosts

• Ensures 'hand' remains hosted in the Spark mesh-lobe shape.

lobes/profiles/builtin_shapes/spark-lobe.toml

thor-lobe.tomlInclude hand in thor-lobe hosts +9/-1

Include hand in thor-lobe hosts

• Ensures 'hand' remains hosted in the Thor mesh-lobe shape.

lobes/profiles/builtin_shapes/thor-lobe.toml

thor-muse.tomlInclude hand in thor-muse hosts +9/-1

Include hand in thor-muse hosts

• Ensures 'hand' remains hosted in the Thor muse-hosting shape.

lobes/profiles/builtin_shapes/thor-muse.toml

thor-worker.tomlInclude hand in thor-worker hosts +9/-1

Include hand in thor-worker hosts

• Ensures 'hand' remains hosted in the Thor worker-hosting shape.

lobes/profiles/builtin_shapes/thor-worker.toml

render.pyUpdate profile rendering for new hand role +1/-0

Update profile rendering for new hand role

• Adjusts profile rendering pipeline to remain consistent with the expanded role set including 'hand'.

lobes/profiles/render.py

schema.pyAdd hand to profile schema role list +15/-1

Add hand to profile schema role list

• Extends per-machine profile schema roles to include 'hand' as a fully-tunable generate lane.

lobes/profiles/schema.py

shape_render.pyUpdate shape rendering for hand default hosting +4/-0

Update shape rendering for hand default hosting

• Updates shape rendering logic to remain consistent with 'hand' being default-hosted across built-in shapes.

lobes/profiles/shape_render.py

shapes.pyUpdate default-hosted role set and invariants for hand +16/-6

Update default-hosted role set and invariants for hand

• Updates COLLEAGUE/DEFAULT_HOSTED role commentary and behavior to incorporate 'hand' as default-hosted.

lobes/profiles/shapes.py

_compose.pyAlign runtime compose logic with hand lane wiring +1/-0

Align runtime compose logic with hand lane wiring

• Updates runtime compose logic to remain consistent with the new hand lane and updated tier naming.

lobes/runtime/_compose.py

docker-compose.ymlAdd default-on vllm-hand lane and wire gateway HAND_* vars +149/-0

[Comment truncated to fit github's 65,536-char limit.]

@qodo-code-review

qodo-code-review Bot commented Aug 10, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Adapter ID collision risk ✓ Resolved 🐞 Bug ≡ Correctness
Description
Gateway now treats LoRA adapter names as first-class served model ids, but it never validates/warns
if an adapter id collides with another backend’s served_name (or another adapter). In a collision,
_backend_for returns the first match by backend order, so a request can be served by an unintended
backend with no explicit error.
Code

lobes/gateway/_routing.py[224]

+            if requested == backend.served_name or requested in backend.adapters:
Relevance

●● Moderate

Collision validation is plausible but adds behavior/policy; no close gateway-routing precedent found
for adapter-id conflicts.

PR-#34

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Routing now considers adapter ids equivalent to served model ids, but collision detection still only
checks served_name, meaning adapter-id collisions can create ambiguous ownership resolved purely
by backend ordering.

lobes/gateway/_routing.py[16-41]
lobes/gateway/_routing.py[204-280]
lobes/gateway/_config.py[572-593]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
LoRA adapter names (`Backend.adapters`) are now treated as routable served model ids. If an adapter id duplicates another backend’s `served_name` (or another adapter id), routing resolves ownership by first match and can silently pick the wrong backend.

## Issue Context
- Routing ownership checks now include `requested in backend.adapters`.
- Config-time collision warnings only look at `backend.served_name`, not adapter ids.

## Fix Focus Areas
- lobes/gateway/_routing.py[204-280]
- lobes/gateway/_config.py[572-593]

## What to change
1. Extend `_warn_on_served_name_collisions` (or add a new validator) to build a single map of **all model ids a backend claims**:
  - `{backend.served_name} ∪ set(backend.adapters)`
2. Warn (or raise, if you want to fail-fast) when any id is claimed by more than one backend.
3. Consider also warning when an adapter id collides with an existing alias key (since aliases take precedence in `resolve_model`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. Hand wiring comment mismatch ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
build_config’s new hand backend comment says it is wired when HAND_BASE_URL or
HAND_SERVED_NAME is present, but _optional_backend only wires when the *_BASE_URL env var is
set. This can mislead operators into thinking HAND_SERVED_NAME alone activates the backend.
Code

lobes/gateway/_config.py[R622-625]

+        # Wired when HAND_BASE_URL or HAND_SERVED_NAME is present. It is
+        # default-HOSTED (every rendered card profile declares it), so on a
+        # freshly-inited deployment these are always set; it stays an
+        # _optional_backend anyway so a pre-hand `.env` — which has neither —
Relevance

●●● Strong

Team often fixes comments/docs that contradict runtime config behavior; mismatch is clear and
low-risk.

PR-#142
PR-#83

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The comment claims served-name-only wiring, but the wiring helper _optional_backend explicitly
returns None unless the URL key is set, so the comment is inaccurate.

lobes/gateway/_config.py[611-638]
lobes/gateway/_config.py[536-563]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The comment above the `hand` backend construction claims wiring happens when `HAND_BASE_URL` **or** `HAND_SERVED_NAME` is present, but `_optional_backend` returns `None` unless `HAND_BASE_URL` is set.

## Issue Context
This is documentation drift that can cause incorrect operator expectations during manual env edits.

## Fix Focus Areas
- lobes/gateway/_config.py[536-563]
- lobes/gateway/_config.py[617-638]

## What to change
- Update the comment to state that `HAND_BASE_URL` must be set to wire the backend (and optionally note that `HAND_SERVED_NAME` only affects the name once wired).
- Alternatively (less recommended given the existing “phantom backend” rationale), change `_optional_backend` semantics if you truly want served-name-only wiring.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context used
✅ Compliance rules (platform): 95 rules

Grey Divider

Tip of the day
💡 Did you know, you can reply 'qodo' on any finding to push back, ask questions, or dig deeper

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread lobes/gateway/_routing.py
Comment thread lobes/gateway/_config.py Outdated
…% -> 100% (0.56.2)

SonarCloud's new-code coverage gate failed at 51.2% (needs 80%), and it was
pointing at something real rather than at a metric. The uncovered lines were
almost entirely the LoRA-adapter machinery — the declaration parser, the engine
probe, the cache refresh, the alias derivation and the /v1/models filter. That
is the honesty surface this release is largely *about*, and it had no direct
tests at all. My delivery summary's claim that it was "covered by offline
tests" was overstated; that row is corrected in the same commit.

29 tests across four collaborating pieces, weighted toward the silent failure
mode — an adapter vLLM refused is simply absent from the engine's model list,
so every layer must let that absence propagate rather than paper over it with
the declaration:

- _hand_adapter_names: partition-not-split (a path keeps an "="), malformed
  segments dropped rather than yielding a nameless alias, dedupe, whitespace
- probe_backend_adapters: the intersection; ids lobes never declared cannot be
  injected; an empty declaration opens no socket; correct path; NO api key
  (co-resident lane, not a cross-box peer); and fail-closed on non-200,
  unreachable, and malformed/wrong-shape bodies
- ReadinessCache: empty seed with no probe at construction, snapshot copy
  isolation, and one raising probe degrading to empty WITHOUT aborting the pass
- the hand:<domain> aliases derived from the same declaration the engine reads,
  an undeclared adapter getting no alias (which is what produces the 404 rather
  than a silent fall-back to the base), and list_models_payload listing only
  CONFIRMED adapters — declared-but-unconfirmed is invisible, and an adapter
  cannot outlive an unready lane
- _resolve_model picking the hand role, asserting the cheap tier is no longer
  the demoted 4B

2893 -> 2922 tests.

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

Addresses both Qodo findings on #184 and the SonarCloud S3776 issue.

**Qodo 1 — adapter id collision (valid, fixed).** `_backend_for` matches
`requested == backend.served_name or requested in backend.adapters`, so an
adapter name is an ownership claim exactly like a served name is. But
`_warn_on_served_name_collisions` only indexed `served_name`, so an adapter
colliding with another backend's served name resolved by backend order with no
warning at all. It now indexes every id a backend claims (`_claimed_model_ids`),
and the message names the remedy that actually applies — telling an operator to
change `*_SERVED_NAME` is useless advice when the duplicate came from
`HAND_LORA_MODULES`.

Qodo also suggested warning on adapter-vs-alias collisions, which is a distinct
and arguably worse case: `resolve_model` checks aliases FIRST, so an adapter
named `cortex` is not order-dependent, it is totally unreachable. That gets its
own warning and its own message.

**Qodo 2 — misleading comment (valid, fixed).** My comment said the hand
backend is wired when `HAND_BASE_URL` **or** `HAND_SERVED_NAME` is present.
`_optional_backend`'s own docstring says the opposite in as many words: the
name key alone is not enough, because a served name with no URL describes a
model rather than a reachable backend, and wiring one invents a phantom backend
advertised on /v1/models that no request can reach. The comment contradicted
the function it was documenting.

**Sonar S3776.** `build_config` was at cognitive complexity 18 (limit 15); my
`hand` alias loop is what pushed it over. Rather than trim just my own
contribution to land exactly on the limit, three alias derivations are now
named helpers — `_hand_adapter_aliases`, `_add_self_named_opt_in_aliases`,
`_add_pooling_role_aliases` — taking it well clear. Behaviour-preserving: the
existing alias tests pass unchanged, and the comments explaining *why* each
alias exists (the WRONG VECTOR SPACE hazard for embed-deep, the checkpoint-swap
survival argument for the pooling role names) moved into the helpers' docstrings
rather than being dropped.

6 new tests: adapter-vs-served-name collision, the served-name path not
regressing into adapter advice, no false positive when ids are distinct,
alias shadowing, the `hand:<domain>` alias not reporting itself, and the
extracted helper in isolation. 2922 -> 2928. New-code coverage stays 100%.

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

Copy link
Copy Markdown
Contributor Author

Valid, and fixed in b44fe32 — thank you, this was a real hole.

You identified the asymmetry exactly: _backend_for matches requested == backend.served_name or requested in backend.adapters, so an adapter name is an ownership claim of precisely the same kind as a served name, but _warn_on_served_name_collisions only ever indexed served_name. A collision between the two kinds resolved by backend order with no warning at all.

What landed:

  • A new _claimed_model_ids(backend) returning (served_name, *adapters), and the warner now indexes every id a backend claims. This also catches a single backend claiming an id twice (an adapter named after its own base), which resolves by first match just the same.
  • The message now names the remedy that actually applies. Telling an operator to change *_SERVED_NAME is useless advice when the duplicate came out of HAND_LORA_MODULES, so the remedy branches on whether every claimant reached the id via its served name.

I also took your third suggestion, and I think it is the more dangerous of the two cases: resolve_model checks table.aliases first, so an adapter named after a tier, a role, or an operator's GATEWAY_ALIASES key is not merely order-dependent — it is totally unreachable by its own name. That gets its own check (_warn_on_adapter_alias_shadowing) and its own message, called after the GATEWAY_ALIASES merge so operator aliases are covered too, not just the built-in ones.

I went with warn rather than fail-fast, matching the existing collision behaviour: the gateway starting is what lets an operator read the warning and fix the env, and a hard failure here would take down a fleet over a misnamed adapter.

Six tests cover it, including that the hand:<domain> alias we deliberately mint does not report itself as shadowing the adapter it exists to reach.

  • lobes (Claude)

@OriNachum

Copy link
Copy Markdown
Contributor Author

Correct, and fixed in b44fe32.

The comment was worse than drift — it contradicted, in as many words, the docstring of the very function it sat above. _optional_backend says the name key alone is NOT enough, and gives the reason: a served name with no URL describes a model, not a reachable backend, and wiring one anyway invents a phantom whose base_url falls back to a hardcoded compose service that need not exist, advertised on /v1/models yet unreachable on every request.

That is exactly the #92 honesty failure this repo cares most about, so your read of the operator risk is right: someone setting HAND_SERVED_NAME alone and trusting my comment would conclude the lane was wired.

I took your recommended option — fix the comment, not the semantics. The phantom-backend rationale is load-bearing and shared by every _optional_backend caller; hand should not be the one lane with different wiring rules. The comment now states that HAND_BASE_URL alone wires the lane and that HAND_SERVED_NAME only names it once wired.

  • lobes (Claude)

…ch (0.56.4)

Two markdownlint errors, both self-inflicted in CHANGELOG prose:

- MD033: `hand:<domain>` unbackticked parses as inline HTML `<domain>`
- MD037: bare `_backend_for`, `_hand_adapter_aliases`, `*_SERVED_NAME` etc.
  parse as emphasis markers

Worth naming the process error rather than just the fix: my local markdownlint
run passed because I ran it BEFORE `bump.py` appended the entry that contained
the violations. Lint the changelog after the bump, not before.

Sonar S8997 (x2): the adapter-probe test rebound a module global by hand with a
try/finally restore. It now uses the `monkeypatch` fixture, which is both the
convention and safer — restoration survives an exception in the assertions, not
just in the call under test. A sibling test also drops its inline
`contextlib.redirect_stderr` for the `capsys` fixture the rest of the module
already uses.

Sonar's remaining S3776 report is stale: it points at `_config.py:596`, which
was `build_config`'s line before the previous commit extracted three helpers
out of it. That function now starts at 668 and scores well under the limit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PcC3i2xBuGcVPPH4tTr5pv
`assert "WRONG " in err and "VECTOR SPACE" in err` was only two clauses because
the expected message wraps across two source lines; the emitted text is one
contiguous "WRONG VECTOR SPACE", so a single substring check is both simpler
and a strictly stronger assertion — the composite form would have passed on
output where the two fragments appeared far apart.

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

Copy link
Copy Markdown

@OriNachum
OriNachum merged commit 86967f4 into main Aug 10, 2026
10 checks passed
@OriNachum
OriNachum deleted the feat/hand-lobe-lfm2.5-1.2b branch August 10, 2026 12:21
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