Lands Sage's sage/row66-r5 at 7ffb640ac, round 4 (r4d82c830779) rebased ontoa36523713after row 197, approved by Dewey (comment 30085) and Filbert (30086) on manifest 6cfd812da4ca (23 paths, rev 2905). The rebase had no conflict. The range-diff is = except one pi-session.test.mjs hunk header. Patch-id without agents/ is d67275ac4251 on7b9737953,a36523713and this commit's parent. Gate (mos-1186): unslop, harness, cli and every scripts/test-*.sh green on r4d, harness 149/0 again on r5. Landing sessions on a scratch root from the candidate tree: Claude Code r4ab24630d323 showed the note, the injected notice and counts checked 2, flagged 1, notices 1. Pi r1dd94171bede is launch only until #383 (Vikunja #454).
Mosaic runtime adapters
An adapter is the entire harness-specific surface of the system. Everything upstream of an adapter — configuration, contracts, missions, tasks, run records — is harness-agnostic; everything inside an adapter may assume one specific agent runtime.
Contract
An adapter lives at:
/opt/mosaic/adapters/<name>/adapter.sh
and must be executable. The dispatcher (/opt/mosaic/src/run-agent.sh)
selects it via MOSAIC_ADAPTER (default: pi) and execs it after the
system prompt has been generated.
Inputs (environment):
| Variable | Meaning |
|---|---|
MOSAIC_SYSTEM_PROMPT_FILE |
Absolute path to the generated system prompt (contracts + optional mission section). Read it; do not modify it. |
MOSAIC_REQUEST |
The exact user request text (may contain newlines). |
MOSAIC_PROVIDER |
Configured provider name. |
MOSAIC_MODEL |
Configured model id. |
Optional, adapter-specific (documented per adapter):
| Variable | Meaning |
|---|---|
MOSAIC_MOCK_RESPONSE |
mock only: the verbatim response to emit |
Outputs:
stdout: the model response text — the only channel the orchestrator capturesstderr: diagnostics (never credentials)- exit
0: success; nonzero: failure
Rules
- Adapters print ONLY the response on stdout. Status lines go to stderr.
- Adapters never read configuration files; the resolved settings arrive via environment.
- Adapters never write outside
/var/lib/mosaic. - Adding a worker adapter requires: a new directory, the contract
implementation, and adding the name to the allowlist in
scripts/mosaic-config.mjs. A managed-session adapter (below) is selected by the launch bundle, not byexecution.adapter, and is not on that allowlist unless it also works as a worker adapter.
Included adapters
pi— the pinned@earendil-works/pi-coding-agentCLI in noninteractive print mode (-p), ambient discovery disabled, stdin detached.mock— deterministic echo ofMOSAIC_MOCK_RESPONSE. Test-only: never use it where a real model response is required.claude— the host'sclaudeCLI, for managed sessions only (below):MOSAIC_CLAUDE_BINwhen set, elseclaudeonPATH. Not a worker adapter: the image has noclaude, and the adapter refuses without the bundle's hook and MCP files.
Managed sessions (slice 1 S6)
The session runner (packages/harness/src/runner.mjs) runs one adapter
call per turn on the host, as /bin/sh <adapters/<name>/adapter.sh> (the
repository keeps adapters 0644; only the image sets the mode), in the
session's workspace and its own process group. Same contract, plus:
| Variable | Meaning |
|---|---|
MOSAIC_WORKSPACE |
The session's workspace; the adapter runs there. |
MOSAIC_SESSION_DIR |
The session's persistent directory; later turns resume the session kept there. |
MOSAIC_TOOLS |
The built-in tool limit (pi names for pi, Claude Code names for claude), comma-separated. |
MOSAIC_POLICY_FILE, MOSAIC_TOOLS_FILE |
The bundle's gate policy and typed tools. |
MOSAIC_TOOL_SOCKET |
The runner's tool socket, which the typed tools call. |
MOSAIC_TURN_MARKER |
pi: the extension writes it at agent_end; a pi turn that exits 0 without it failed. |
MOSAIC_EXTENSIONS |
pi only: extension files loaded with -e; a missing one exits 2. The pi module names two: the gate and typed tools extension, then the unslop check's tools/unslop-hook/extension.ts (row 66). |
MOSAIC_UNSLOP_LOG |
The unslop check's JSONL log, <run>/unslop.jsonl, which the runner sets for both adapters and reads after each turn (row 66). |
MOSAIC_CLAUDE_SETTINGS, MOSAIC_CLAUDE_MCP_CONFIG |
claude only: the bundle's gate hook and MCP server; required. |
MOSAIC_THINKING |
The route's thinking level (row 65), which the harness module's levels() already checked. pi passes --thinking (off, minimal, low, medium, high, xhigh, max) and omits it when unset, as for run-task.sh workers. claude passes --effort (low, medium, high, xhigh, max) and requires it; unset exits 2. |
MOSAIC_CLAUDE_BIN |
claude only: absolute path of the CLI to run, from the host's environment (the bus unit pins it). Unset or empty runs claude from PATH; a relative path or a non-executable file exits 2. |
The layers each adapter relies on, and what they don't cover, are in
packages/harness/README.md.
A role's skills (row 64)
A managed session's skills reach both adapters as text in the bundle's
prompt.md, which they pass as a file: pi with --system-prompt <path>
and claude with --system-prompt-file <path> (row 213). Neither adapter loads a
skill of its own: pi keeps --no-skills, since role sessions never get
MOSAIC_SKILLS, and claude keeps --disable-slash-commands and
--restricted. Sage chose this on 2026-10-19 from a probe, recorded in
agents/sage/work/queue-64/probe.md:
- Pi's
--skillputs only a skill's name and description in the prompt, and the model has toreadthe SKILL.md itself. The gate refuses every file-tool path outside the workspace, so the session couldn't open its own skills. Opening the gate to the bundle would change a security boundary. - Claude Code loads a bundle-local
--plugin-dirskill under--restrictedonly with itsSkilltool on, and then it also offers its own bundled skills. Which ones it offers changes with the Claude Code version, and theSkilltool would pass through the gate hook.
The cost is prompt size: a role's skill text is in every turn, where the
other routes load a skill when it's used. The bundle caps a role's skills at
64 KiB together. The cap dates from when claude passed prompt.md as one
argument, which Linux caps at 128 KiB; row 213 passes the file instead, so no
local user reads the prompt in /proc/<pid>/cmdline. Pi 0.85.1 uses the path
string itself as the prompt when the file is missing or can't be read, so the
pi adapter exits 2 unless the path is a readable regular file.
The unslop check (row 66)
Both adapters run the mechanical writing check from tools/unslop-hook,
driven by one lists.json. pi loads it as its second -e extension.
claude runs claude-hook.js from the bundle's settings.json: the Stop
hook checks the turn's replies, and the UserPromptSubmit hook adds the
correction notice as context on the next turn when the last reply had
tells. Each hook command ends ; exit 0, so neither blocks. Both log to
MOSAIC_UNSLOP_LOG, and the runner adds a note for the operator to the
next RESULT it sends (packages/harness/README.md, the runner). The
harness modules mount the three files read-only at their own paths, and
the manifest records their digests. MOSAIC_UNSLOP_HOOK and UNSLOP_LISTS
aren't in the session environment's allowlist, so a managed session can't
switch the check off or swap its lists.