#!/bin/sh # Pi adapter: implements the Mosaic adapter contract for the pinned # @earendil-works/pi-coding-agent CLI. # # Contract: see /opt/mosaic/adapters/README.md. # Headless (default): stdout = response only; stderr = diagnostics; exit 0. # Interactive (MOSAIC_INTERACTIVE=1): full pi TUI on the attached terminal. set -eu [ -n "${MOSAIC_SYSTEM_PROMPT_FILE:-}" ] || { echo "pi adapter: MOSAIC_SYSTEM_PROMPT_FILE is required" >&2; exit 2; } # Row 213 (C2-08): pi gets the prompt as a path, so no local user reads it in /proc//cmdline. # Pi 0.85.1 reads --system-prompt as a file only when the path exists and reads, and otherwise # uses the path string itself as the prompt, without a warning for a missing file # (dist/core/resource-loader.js resolvePromptInput). So the path must be a regular readable # file here, and is made absolute before the cd below, since pi resolves it from its own cwd. PROMPT_FILE=$MOSAIC_SYSTEM_PROMPT_FILE case "$PROMPT_FILE" in /*) ;; *) PROMPT_FILE="$(pwd)/$PROMPT_FILE" ;; esac [ -f "$PROMPT_FILE" ] && [ -r "$PROMPT_FILE" ] || { echo "pi adapter: system prompt not readable: $MOSAIC_SYSTEM_PROMPT_FILE" >&2; exit 2; } # MOSAIC_AGENT_NAME is optional in headless mode (identity section is then # omitted); interactive launches always set it via scripts/agent.sh. : "${PI_PROVIDER:?pi adapter: PI_PROVIDER is required}" : "${PI_MODEL:?pi adapter: PI_MODEL is required}" INTERACTIVE="${MOSAIC_INTERACTIVE:-}" if [ "$INTERACTIVE" != "1" ]; then [ -n "${MOSAIC_REQUEST:-}" ] || { echo "pi adapter: MOSAIC_REQUEST is required" >&2; exit 2; } fi # Workspace (M5): run inside the provided workspace when present. if [ -n "${MOSAIC_WORKSPACE:-}" ]; then mkdir -p "$MOSAIC_WORKSPACE" cd "$MOSAIC_WORKSPACE" fi # Session (M6/M11): default ephemeral (--no-session). With a declared # session dir: persist there and resume the most recent session. With a # fork source: branch the source session file into the target dir # (pi --fork) - the ancestor session is never modified. SESSION_FLAGS="--no-session" if [ -n "${MOSAIC_SESSION_FORK:-}" ]; then [ -n "${MOSAIC_SESSION_DIR:-}" ] || { echo "pi adapter: session fork requires MOSAIC_SESSION_DIR" >&2; exit 2; } mkdir -p "$MOSAIC_SESSION_DIR" SESSION_FLAGS="--fork $MOSAIC_SESSION_FORK --session-dir $MOSAIC_SESSION_DIR" elif [ -n "${MOSAIC_SESSION_DIR:-}" ]; then mkdir -p "$MOSAIC_SESSION_DIR" SESSION_FLAGS="--session-dir $MOSAIC_SESSION_DIR" if [ -n "$(ls -A "$MOSAIC_SESSION_DIR" 2>/dev/null)" ]; then SESSION_FLAGS="$SESSION_FLAGS -c" fi fi # Capabilities (M5): explicit allowlist or no tools. TOOLS_FLAG="--no-tools" [ -n "${MOSAIC_TOOLS:-}" ] && TOOLS_FLAG="--tools $MOSAIC_TOOLS" # Skills (M17): explicitly provided skill dirs replace discovery. When none # are provided the agent runs with --no-skills (nothing ambient to find). # Role sessions never get MOSAIC_SKILLS (ENV_ALLOW in packages/seat/src/ # session.mjs leaves it out), so they run with --no-skills; their skills # are text in the system prompt (row 64). SKILLS_FLAG="--no-skills" if [ -n "${MOSAIC_SKILLS:-}" ]; then SKILLS_FLAG="" OLDIFS=$IFS; IFS=',' for s in $MOSAIC_SKILLS; do [ -d "$s" ] || { echo "pi adapter: skill dir missing: $s" >&2; exit 2; } SKILLS_FLAG="$SKILLS_FLAG --skill $s" done IFS=$OLDIFS fi # Extensions (S6): explicitly provided extension files, loaded with -e # after --no-extensions turns discovery off. A missing file refuses here; # pi itself also refuses to start on a missing or broken -e (row S0 line 3). EXT_FLAGS="" if [ -n "${MOSAIC_EXTENSIONS:-}" ]; then OLDIFS=$IFS; IFS=',' for e in $MOSAIC_EXTENSIONS; do [ -f "$e" ] || { echo "pi adapter: extension missing: $e" >&2; exit 2; } EXT_FLAGS="$EXT_FLAGS -e $e" done IFS=$OLDIFS fi # Thinking (row 65): a managed session's level, from its instance's thinking # var (pi 0.85.1: off, minimal, low, medium, high, xhigh, max). Workers from # run-task.sh set none and keep pi's default. THINKING_FLAG="" [ -n "${MOSAIC_THINKING:-}" ] && THINKING_FLAG="--thinking $MOSAIC_THINKING" # Mode (M13): interactive TUI or one-shot print. PRINT_MODE="-p" REQUEST_ARG="" if [ "$INTERACTIVE" = "1" ]; then PRINT_MODE="" else REQUEST_ARG="$MOSAIC_REQUEST" fi # All flags documented in the pi package README (CLI Reference): # -p/--print one-shot mode: print the response and exit (omitted in # interactive TUI mode) # --system-prompt replace the default prompt with the generated one, given # as the file's absolute path (checked above), never its text # --no-* no ambient context/skills/extensions/templates/themes # EXT_FLAGS the explicitly provided extensions only (-e) # SESSION_FLAGS ephemeral | persistent | forked (per env) # TOOLS_FLAG per capabilities # THINKING_FLAG --thinking for a managed session, else nothing # --offline no startup network operations (update checks/telemetry) # --no-approve ignore project-local .pi/ files (settings, SYSTEM.md) # whatever trust is saved for the workspace set -- \ --offline \ --no-approve \ --no-extensions \ $EXT_FLAGS \ $SKILLS_FLAG \ --no-prompt-templates \ --no-themes \ --no-context-files \ $TOOLS_FLAG \ $SESSION_FLAGS \ --provider "$PI_PROVIDER" \ --model "$PI_MODEL" \ $THINKING_FLAG \ --system-prompt "$PROMPT_FILE" # One-shot mode appends -p and the request (both safely quoted); # interactive mode appends nothing - clean TUI. [ "$INTERACTIVE" = "1" ] || set -- "$@" -p "$MOSAIC_REQUEST" if [ -n "${MOSAIC_MODEL_EVENT_MONITOR:-}" ] && [ "$INTERACTIVE" != "1" ]; then STREAM="$(dirname "$0")/../../packages/plugins/harness/pi/model-stream.mjs" exec node "$STREAM" pi "$MOSAIC_FIRST_OUTPUT_DIR" pi "$@" --mode json fi exec pi "$@"