Claude Code orchestrates, loomcycle executes. A real 10-agent code review through MCP fan-out.
exp7 finished. Different shape from the earlier ops in the operator-via-MCP series (exp1-4 drove Claude Code to test loomcycle; here Claude Code uses loomcycle as its actual runtime). The question: can Claude Code delegate its multi-agent work to loomcycle as a sidecar, instead of carrying it in its own context?
The setup: Claude Code is the operator, the conversation surface, the orchestrator. It doesn't do the heavy work itself. It clones a target repo into a sandbox, synthesizes an agent + a skill from its own .claude/ artifacts, imports them into loomcycle via loomcycle import claude-code, then makes one MCP call that fans 10 reviewer agents out across the loomcycle MCP server. The reviewers do the file-reading. The agents write findings to Memory. One more MCP call spawns a consolidator that merges and returns the report. Claude Code's own context never opens a repo file.
The result: 10/10 slices reviewed across 86 files, 35 issues found (1 Critical + 34 Important), including a race condition in loomcycle's own internal/channels/scheduler.go that's already shipped as a fix in v0.33.0 today. The runtime that just ran the code review got bugs fixed in it by the code review.
This is the architectural shape the v0.23.0 thin-client topology and v0.25.0 agent ensembles have been building toward. exp7 is the first experiment where the answer is clean: Claude Code is the operator. loomcycle is the side runtime where the multi-agent work runs.
The topology
CLAUDE CODE (orchestrator) loomcycle MCP thin client (`loomcycle mcp --upstream`)
1. git clone loomcycle → work/loomcycle-src (read-only review target, in the jail)
2. synthesize .claude/{agents/code-reviewer.md, skills/code-review/SKILL.md}
→ `loomcycle import claude-code --from=work/exp7/.claude --write --skills-dest=$PWD/skills`
→ exp7 config gains the `code-reviewer` agent; the code-review skill is copied + bundled
3. ONE `spawn_runs` (MCP, mode=join, N=10) ──────► loomcycle runtime
│ spawns=[ {agent: code-reviewer, prompt: slice=<name> path=<path>} ×10 ]
│ server-side concurrent, bounded by admission gate
│ join AND-barrier: blocks until all 10 settle → index-aligned envelope
▼
10 × code-reviewer (one per slice), each:
Glob/Grep/Read its slice under loomcycle-src/<path> (READ-ONLY)
→ Memory.set review:<slice>:findings (confidence ≥ 80 only, file:line + fix)
4. ONE `spawn_run` (MCP) → exp7-consolidator
Memory.list "review:" → merge + dedup + count → Memory.set consolidated:report → return
5. Claude Code reads the report (consolidator final_text / consolidated:report) - the delegated result
Three moving parts: the import (how Claude Code's .claude/ shape becomes loomcycle's substrate), the fan-out (one MCP call that spawns 10 concurrent agent runs), and the boundary (Claude Code never reads a repo file).
The import
Claude Code has its own opinions about how an agent is defined: .claude/agents/<name>.md with YAML frontmatter (model, tools, skills) plus a markdown body that becomes the system prompt. .claude/skills/<name>/SKILL.md with a methodology agents can be told to follow. Those shapes have been around long enough that any real Claude Code user has a folder full of them.
loomcycle import claude-code (RFC C2, shipped in v0.12.8) translates that shape into loomcycle's substrate. exp7's invocation:
loomcycle import claude-code \
--from=work/exp7/.claude \
--write \
--skills-dest=$PWD/skills
The mapping:
| Claude Code shape | loomcycle shape |
|---|---|
agents/code-reviewer.md frontmatter tools: | AgentDef allowed_tools |
| agent body (the markdown after frontmatter) | AgentDef system_prompt |
model: sonnet | tier: middle |
skills/code-review/SKILL.md | copied to $PWD/skills/code-review/SKILL.md; appended to the agent's system_prompt at config-load (skills: loaded 1) |
One honest import-fidelity gap: the importer doesn't carry the agent's skills: frontmatter field through ("no loomcycle equivalent"). We re-attached skills: [code-review] by hand after the import. A small follow-up.
What this means in practice: a Claude Code user who already has working agents + skills can run them on loomcycle with one import command. No re-author for the new runtime. The agent's identity (tools, prompt, methodology) survives the move.
The fan-out (RFC Y, v0.33.0)
The original exp7 (a few weeks back) couldn't do this from the outside. It used an in-loomcycle dispatcher agent (an exp7-orchestrator calling Agent op=parallel_spawn) because the MCP plugin couldn't fan out N concurrent blocking spawns. That was the F17 / RFC O head-of-line-blocking problem, with the async spawn_run not yet shipped. The dispatcher worked, but the fan-out lived inside the runtime. Claude Code was just kicking off one run that happened to spawn ten more.
RFC Y (today, v0.33.0, #464) puts the fan-out at the MCP boundary. One MCP call from Claude Code spawns N concurrent loomcycle runs server-side, with the AND-barrier built into the response envelope. The in-loomcycle dispatcher is gone. The fan-out is now genuinely external.
Wire shape:
# One MCP call. 10 children, server-side-concurrent. Bounded by the admission gate.
spawn_runs({
mode: "join", # mode:detach for fire-and-forget awaits RFC P
spawns: [
{ agent: "code-reviewer", prompt: "slice=api path=internal/api/http" },
{ agent: "code-reviewer", prompt: "slice=tools path=internal/tools/builtin" },
{ agent: "code-reviewer", prompt: "slice=provs path=internal/providers" },
... # 10 total, cap 32
]
})
# → returns when all settle, an index-aligned envelope:
[
{ index: 0, run_id: "r_…", status: "completed", final_text: "..." },
{ index: 1, run_id: "r_…", status: "completed", final_text: "..." },
...
]
# Per-child failures captured in-envelope; never fail the batch.
Same wire is also available as REST: POST /v1/runs:batch. Both reuse the executeParallelSpawn core that's been powering in-loomcycle parallel_spawn since v0.10 (same admission gate, same per-child failure capture, same join semantics). The external boundary just got first-class access to a primitive that already existed inside the runtime.
The exp7 reviewer agent's allowed_tools is narrow: [Read, Grep, Glob, Memory, Context]. No Bash, no Write. Read-only by construction. The 10 reviewers can't write to the repo. They can only read it and record findings to Memory. The sandbox is enforced by allowed_tools, not by good intentions.
The result: 10/10 slices, 86 files, 35 issues
The consolidator returned an executive report with categorized findings, file:line citations, and recommended fixes. The Critical was the headline:
Critical · channels · internal/channels/scheduler.go:81A time.AfterFunc closure can fire and run timers.Delete(id) + pendCnt.Add(-1) before the outer LoadOrStore commits the entry. In the sub-µs window between the two, the entry is then stored but its pendCnt is never decremented → permanent pendCnt leak + orphaned entry. Fix: create the timer after a successful LoadOrStore.
Real concurrency bug. Sub-microsecond race window. The kind that doesn't reliably reproduce in tests but would leak over hours of production load. Fix is one statement-reorder. Shipped in #463 today.
The 34 Important findings (confidence ≥ 85 each, all with file:line citations) covered a spread of patterns the runtime had accumulated as it grew. A selection:
store/postgres/postgres.go:5976:newID()panic()s oncrypto/randfailure on the hotCreateSession/CreateRun/AppendEventpath. A transient entropy error crashes the process. Should return an error instead.pause/manager.go:524:ToolCtx(StatePausing/Paused branch) returns the barecancelas its "cleanup" callback, never callsactiveTools.Delete(id). The finalize-pause Stage-2 barrier spins to deadline waiting for an entry that nothing is going to delete.snapshot/restore.go:500: restored paused runs are inserted withStatus=RunRunninginstead ofPaused. The resume dispatcher (filters by status) never picks them up. Run stuck "running" forever. Direct regression risk for the v0.30.0 F42 cross-instance mid-run resume we shipped yesterday.tools/builtin/memory.go:884:execMerge/execAppendDedupe/execBoundedListruncheckQuotaafterMemoryAtomicUpdatecommits. Unbounded storage growth past the operator quota. Quota check is supposed to be a precondition, not a postcondition.api/http/channels_admin.go:99(and siblings): admin POST handlers decoder.Bodywith nohttp.MaxBytesReader. An authenticated client can OOM the server by streaming a multi-GB request body. Bearer-authed but still an availability vector for any operator who lets non-trusted users hold a token.api/http/server.go:3932: the interactive-run background goroutine'sdefer release()fires when the HTTP handler returns, not when the goroutine itself finishes. Concurrency-semaphore slot leak. Direct regression risk for v0.27.0's detached-interactive-run feature.providers/anthropic_oauth_dev/refresh.go:74:Refresher.Stop()deadlocks if called beforeStart()(blocks on adoneChonlyStart's goroutine closes). The "always safe to defer Stop()" contract is false on bootstrap-failure paths.
Plus systematic themes the consolidator surfaced across the 10 reviewer reports: context.Background() where a caller/timeout context is required (api-http, channels, cmd, providers); silent error discards (snapshot ExportPretty checksum, store dimensions unmarshal, scheduler shutdown hooks); dead code (ctxDone, nilEmbedding, idempotentBuiltins, forceCancelMu).
Same-day fixes. The security findings (C1/C2 tenant scope + infra-secret YAML expansion + I6/I1 input-endpoint scope) shipped as #462. The crash/correctness/hardening findings (C3/I3/I5/I7/I2/I4 scheduler error log + channel point-lookup + snapshot checksum + 3 hardening fixes) shipped as #463. Both merged before this post landed. Of the 35 findings, the highest-severity ten are closed. The long tail is filed for triage in doc-internal/rfcs/exp7-code-review-findings.md.
The self-validating loop: Claude Code orchestrated 10 loomcycle agents to review the loomcycle runtime. The agents found real bugs. The runtime got better the same day. The thing that ran the experiment got fixed by the experiment.
Three runtime findings the experiment itself surfaced
Beyond the code-review payload, driving a real concurrent MCP fan-out (vs. the original in-loomcycle dispatcher topology) stressed the runtime in three ways worth handing back to the dev team:
1. Glob doesn't match absolute paths inside the read-root
Read-only Glob sandbox gapReviewers given an absolute glob /Users/.../work/loomcycle-src/<path>/**/*.go got zero matches, and under concurrent load concluded "repo not mounted" and bailed. The relative form loomcycle-src/<path>/**/*.go (with cwd = read-root) matched correctly.
First fan-out: 5/10 slices recorded findings. The other 5 hit the absolute-path Glob gap and gave up. After we switched the reviewer prompt to relative paths consistently, all 10 found files. A real correctness gap in the read-only Glob sandbox semantics. Should normalize absolute-into-read-root paths or fail loud rather than match-nothing.
2. Cross-provider fallback drops the thinking / reasoning_content block
Provider-fallback fidelity gapWhen 10 sonnet reviewers saturated the Claude OAuth account (HTTP 429), the runtime fell back Anthropic → DeepSeek mid-conversation. DeepSeek rejected the request with openai 400 ... "reasoning_content in the thinking mode must be passed back to the API." The fallback path doesn't carry the in-flight reasoning block that DeepSeek's thinking mode requires.
Real fallback-fidelity bug. The fallback path translates the Anthropic conversation shape to OpenAI-compatible, but the thinking / reasoning_content block is provider-specific and gets dropped. DeepSeek's thinking-mode contract requires it. Handed to the dev team as a follow-up RFC.
3. Fan-out sizing vs. OAuth subscription rate limit (operational, not a bug)
10 concurrent heavy reviewers each reading 15-100 files exceeded the Claude subscription's rate limit (HTTP 429). The fix is not in the code. It's an admission-gate sizing decision: max_concurrent_runs: 4 + queue_timeout_ms: 1_200_000 so queued children wait for a slot instead of getting rejected. The spawn_runs join still takes all 10. The rest queue and run as slots free. Result: 10/10 completed, zero 429s.
Lesson: when fanning out heavy LLM work to an OAuth subscription (vs. a paid API key with higher limits), throttle the admission gate. The runtime supports it. The operator has to configure it.
What this architectural shape unlocks
Claude Code stays the operator and the conversation surface. loomcycle is the side runtime where the actual multi-agent work runs. Claude Code's context never had to carry the 86 files of the loomcycle repo. Ten loomcycle agents did. The result came back as a single consolidated report. The contract between the two systems is the MCP wire surface. Narrow, structured, well-defined.
loomcycle owns its own loop, its own memory, its own observability, its own per-run credential isolation. Ten reviewer agents ran inside loomcycle, wrote to a shared Memory ledger, and converged through one consolidator call. All without Claude Code's context ever carrying the 86 files.
The practical reading: if you already drive most of your day through Claude Code and you want a real multi-agent runtime that complements it, loomcycle is the shape that fits. The import command brings your existing .claude/agents + .claude/skills over with one invocation. The MCP fan-out (spawn_runs) lets you offload N-wide concurrent agent work in one call. The Memory primitive lets the agents converge on a shared ledger. The consolidator returns the result. Claude Code never loses the orchestrator seat.
Try it
Latest release is v0.33.0 (today). The exp7 example ships as a self-contained directory at loomcycle/examples/exp7-code-review-fanout/. Its own loomcycle.yaml, run.sh, .env.local.example, and reproducible README. Anthropic-OAuth-primary with a DeepSeek fallback, same shape as every prior experiment in the series. MCP tool surface is in doc/MCP.md. The new wire shape is spawn_runs (and POST /v1/runs:batch for REST callers).
Companion reading from the operator-via-MCP series: exp1+2 (the framing of "Claude Code as operator"), exp3 side analysis - the MCP wedge (F17 / RFC O, the head-of-line-blocking gap that made the original exp7 use an in-loomcycle dispatcher; RFC Y closes the same gap from the other direction), exp5 - agent ensembles (RFC S) (the in-loomcycle equivalent shape, Channel.await fan-in), today's earlier post on context compaction (v0.32.0) (the other thing that shipped today).