#!/usr/bin/env bash # Host-development implementation for scripts/agent.sh --host-dev . set -euo pipefail REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$REPO" usage() { echo 'Usage: scripts/agent.sh --host-dev AGENT [--fresh] [--check] [--soul FILE] [--constitution FILE] [--user FILE] [--provider NAME] [--model ID] [--thinking LEVEL]' echo 'Default: resume the latest agent development conversation; first launch starts one.' } fail() { echo "${AGENT:-tui}: $*" >&2; exit 1; } case "${1:-}" in --help|-h) usage; exit 0 ;; "") usage >&2; exit 1 ;; esac AGENT="$1" shift [[ "$AGENT" =~ ^[a-z0-9][a-z0-9._-]{0,63}$ ]] || fail "invalid agent name" [ -d "$REPO/agents/$AGENT" ] || fail "unknown agent: $AGENT" SOUL="$REPO/agents/$AGENT/SOUL.md" CONSTITUTION="$REPO/contracts/CONSTITUTION.md" USER_FILE="" PROVIDER="" MODEL="" THINKING="" FRESH=false CHECK=false while [ "$#" -gt 0 ]; do case "$1" in --fresh) FRESH=true; shift ;; --check) CHECK=true; shift ;; --provider|--model|--thinking) [ "$#" -ge 2 ] && [ -n "$2" ] && [[ "$2" != --* ]] || fail "$1 needs a value" case "$1" in --provider) PROVIDER="$2" ;; --model) MODEL="$2" ;; --thinking) THINKING="$2" ;; esac shift 2 ;; --soul|--constitution|--user) [ "$#" -ge 2 ] && [ -n "$2" ] || fail "$1 needs a file" case "$1" in --soul) SOUL="$2" ;; --constitution) CONSTITUTION="$2" ;; --user) USER_FILE="$2" ;; esac shift 2 ;; --help|-h) usage; exit 0 ;; *) usage >&2; fail "unknown argument: $1" ;; esac done # Reuse the deployment's validated provider/model and live user profile. source "$REPO/scripts/common.sh" load_config [ "$MOSAIC_ADAPTER" = pi ] || fail 'native launcher requires the pi adapter' PROVIDER="${PROVIDER:-$MOSAIC_PROVIDER}" MODEL="${MODEL:-$MOSAIC_MODEL}" THINKING_ARGS=() if [ -n "$THINKING" ]; then case "$THINKING" in off|minimal|low|medium|high|xhigh|max) THINKING_ARGS=(--thinking "$THINKING") ;; *) fail "invalid thinking level: $THINKING" ;; esac fi USER_FILE="${USER_FILE:-$MOSAIC_DATA_ROOT/user/USER.md}" PI="$REPO/node_modules/.bin/pi" [ -x "$PI" ] || fail 'run npm ci --ignore-scripts --no-audit --no-fund in the repository first' PIN="$(node -p "require('./package.json').dependencies['@earendil-works/pi-coding-agent']")" [ "$("$PI" --version)" = "$PIN" ] || fail "local Pi must match package.json ($PIN); run npm ci" command -v flock >/dev/null || fail 'flock is required' CONTEXT_FILES=("$CONSTITUTION" "$REPO/contracts/STANDARDS.md" "$SOUL" "$USER_FILE" "$REPO/AGENTS.md" "$REPO/agents/$AGENT/CONTEXT.md") for file in "${CONTEXT_FILES[@]}"; do [ -f "$file" ] && [ -r "$file" ] && [ -s "$file" ] || fail "missing, unreadable or empty context: $file" done SKILLS=(ms-tools ms-file-read ms-file-write ms-conductor ms-communications ms-agent-watch ms-sdlc ms-proactive-agent ms-goal ms-unslop) SKILL_ARGS=() for skill in "${SKILLS[@]}"; do [ -s "$REPO/skills/$skill/SKILL.md" ] || fail "missing skill: $skill" SKILL_ARGS+=(--skill "$REPO/skills/$skill") done [ -s "$REPO/extensions/goal/index.ts" ] || fail 'missing canonical goal extension' STATE="$REPO/.pi/state/$AGENT" echo "$AGENT: native host TUI | Pi $PIN | $PROVIDER/$MODEL${THINKING:+:$THINKING}" echo "$AGENT: workspace $REPO" echo "$AGENT: SOUL=$SOUL | CONSTITUTION=$CONSTITUTION | USER=$USER_FILE" echo "$AGENT: sessions $STATE/sessions | skills: ${SKILLS[*]}" if "$CHECK"; then echo "$AGENT: configuration checks passed (no TUI, authentication request or model call)" exit 0 fi [ -t 0 ] && [ -t 1 ] || fail 'launch from an interactive terminal (or use --check)' umask 077 mkdir -p "$STATE/sessions" "$STATE/launches" exec 8>"$STATE/launch.lock" flock -n 8 || fail "another $AGENT TUI from this launcher is active" # The native packaging path verifies source copies and refuses installation drift. scripts/sync-dev-extensions.sh SESSION_ARGS=(--session-dir "$STATE/sessions") shopt -s nullglob dotglob SESSION_FILES=("$STATE/sessions/"*.jsonl) SESSION_ENTRIES=("$STATE/sessions/"*) if ! "$FRESH"; then if [ "${#SESSION_FILES[@]}" -gt 0 ]; then # Pi --continue otherwise silently skips broken files and may start fresh. node "$REPO/agents/$AGENT/validate-sessions.mjs" "${SESSION_FILES[@]}" SESSION_ARGS+=(--continue) echo "$AGENT: resuming latest development conversation with current launch context" elif [ "${#SESSION_ENTRIES[@]}" -gt 0 ]; then fail 'session directory has state but no JSONL conversation; inspect it before using --fresh' else echo "$AGENT: starting first development conversation" fi else echo "$AGENT: starting a fresh conversation; earlier conversations are preserved" fi # A private snapshot per launch avoids the container loader's shared prompt file. LAUNCH="$(mktemp -d "$STATE/launches/launch.XXXXXXXX")" PROMPT="$LAUNCH/context.md" for file in "${CONTEXT_FILES[@]}"; do printf '\n===== %s (%s) =====\n' "$(basename "$file")" "$file" >> "$PROMPT" cat "$file" >> "$PROMPT" printf '\n' >> "$PROMPT" done sha256sum "$PROMPT" > "$LAUNCH/context.sha256" echo "$AGENT: context snapshot $PROMPT" unset MOSAIC_LAUNCH_INCARNATION export MOSAIC_AGENT_NAME="$AGENT" # Explicit resources only; preserve Pi's built-in coding prompt and tool guidance. # goal_report must be in the tool allowlist as well as the extension being loaded. exec "$PI" --approve --offline --no-context-files --no-extensions --no-skills \ --no-prompt-templates --no-themes \ --extension "$REPO/.pi/extensions/goal/index.ts" \ "${SKILL_ARGS[@]}" \ --tools read,bash,edit,write,grep,find,ls,goal_report \ --provider "$PROVIDER" --model "$MODEL" "${THINKING_ARGS[@]}" \ --append-system-prompt "$PROMPT" "${SESSION_ARGS[@]}"