Files

140 lines
5.8 KiB
Bash

#!/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/<pid>/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 <level> 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 "$@"