feat: hand — the ninth Colleague role and the fleet's fine-tuning base, on LiquidAI LFM2.5-1.2B (0.56.1) - #184
Conversation
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
|
/agentic_review |
PR Summary by QodoAdd
AI Description
Diagram
High-Level Assessment
Files changed (126)
|
Code Review by Qodo
1.
|
…% -> 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
|
Valid, and fixed in b44fe32 — thank you, this was a real hole. You identified the asymmetry exactly: What landed:
I also took your third suggestion, and I think it is the more dangerous of the two cases: 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
|
|
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. That is exactly the #92 honesty failure this repo cares most about, so your read of the operator risk is right: someone setting I took your recommended option — fix the comment, not the semantics. The phantom-backend rationale is load-bearing and shared by every
|
…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
|



What
Adds
hand— the fleet's ninth Colleague role and its fine-tuning base — onLiquidAI/LFM2.5-1.2B-Instruct.The metaphor is muscle memory: one cheap base, many LoRA adapters, each mastering a domain.
workeris an untrained generalist doer;handis 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:minor/cheaptier, displacingQwen/Qwen3.5-4B(which stays in the catalog as a plain candidate — cite-don't-delete);handrequest is always served, even whencortex/senses/worker/museare shedding 429s;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 withholdsrepo_actiondeliberately — 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 tagvllm-minorrides.Three lane flags are deliberately omitted, each for a reason a reviewer should be able to check:
--quantizationquantization="none"bf16 sentinel. Passing it empty is a parse error; passingmodeloptcorrupts the weights. Live engine confirmsquantization=None--language-model-only--reasoning-parserLFM2.5-1.2B-Thinkingis a separate checkpoint. Arming a reasoning parser here is arming half a pair with no other halfTool calling — the gemma4 trap, avoided
LFM2's
<|tool_call_start|>/<|tool_call_end|>are special tokens. That is precisely the trap that madepythonicsilently wrong for Gemma 4: the parser ran withskip_special_tokens=True, matched nothing, and vLLM relayed a well-formed tool call as ordinary assistant content withtool_calls: null. Callers got prose shaped like a tool call.vLLM ships a purpose-built
lfm2parser 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:
…with the tokenizer delimiters verified present as ids 10 / 11.
LoRA — armed, inventory empty
--enable-loraships 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-modulesand the gateway'shand:<domain>alias derivation — so the two cannot disagree. Fixed at boot; no runtime hot-load.model=handserves the base and never 404s on an empty inventorymodel=hand:<domain>serves that adapterhand:<domain>is refusedmodel_not_found— never silently downgraded to the baseAdapter honesty is enforced by probing the engine's own
/v1/models, not the gateway's filesystem — adapter paths are mounted intovllm-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.
docs/evidence/2026-08-10-partial-hand-orin.txtdocs/evidence/2026-08-10-partial-hand-spark.txtbaseFunctional 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, composehealthy.Why no budget reproduces
Three Spark boots, minutes apart, identical lane, identical
gpu_mem_util = 0.06: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:
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.
handbooted and served at0.06on 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
Missing cudagraph off-switch (
d5). The lane was the only one of eight not settingVLLM_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.A dead knob (
d8).HAND_ATTENTION_BACKENDwas rendered into.envby 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-rolleddocker runprobes 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.py—TIER_ROLEorder is load-bearing:tier_aliasesderives ascending capability from each role's last occurrence (hand < multimodal < worker < muse < primary). Reordering silently changes the tier ladder.lobes/roles.py—GATEWAY_FRONTED_ROLESis now derived fromROLE_ROLE_HINTrather 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.8workerpeer-proxy bug, which is why the per-table completeness tests are parametrised overROLES.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
afi cli doctor . --strictall cleanHAND_*additions only, zero deletionsFollow-ups
handrepo_actiononce adapters existagentculture/unsloth-cli#16