#!/usr/bin/env bash set -euo pipefail # mosaic — Unified agent launcher and management CLI # # AGENTS.md is the global policy source for all agent sessions. # The launcher injects a composed runtime contract (AGENTS + runtime reference). # # Usage: # mosaic claude [args...] Launch Claude Code with runtime contract injected # mosaic opencode [args...] Launch OpenCode with runtime contract injected # mosaic codex [args...] Launch Codex with runtime contract injected # mosaic yolo [args...] Launch runtime in dangerous-permissions mode # mosaic --yolo [args...] Alias for yolo # mosaic init [args...] Generate SOUL.md interactively # mosaic doctor [args...] Health audit # mosaic sync [args...] Sync skills # mosaic macp [args...] Manual MACP queue operations # mosaic seq [subcommand] sequential-thinking MCP management (check/fix/start) # mosaic bootstrap Bootstrap a repo # mosaic upgrade release Upgrade installed Mosaic release # mosaic upgrade check Check release upgrade status (no changes) # mosaic upgrade project [args] Upgrade project-local stale files MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" VERSION="0.1.0" usage() { cat < [args...] Agent Launchers: claude [args...] Launch Claude Code with runtime contract injected opencode [args...] Launch OpenCode with runtime contract injected codex [args...] Launch Codex with runtime contract injected yolo [args...] Dangerous mode for claude|codex|opencode --yolo [args...] Alias for yolo Management: init [args...] Generate SOUL.md (agent identity contract) doctor [args...] Audit runtime state and detect drift sync [args...] Sync skills from canonical source macp [args...] Manual MACP queue operations seq [subcommand] sequential-thinking MCP management: check [--runtime ] [--strict] fix [--runtime ] start bootstrap Bootstrap a repo with Mosaic standards upgrade [mode] [args] Upgrade release (default) or project files upgrade check Check release upgrade status (no changes) release-upgrade [...] Upgrade installed Mosaic release project-upgrade [...] Clean up stale SOUL.md/CLAUDE.md in a project PRD: prdy PRD creation and validation init Create docs/PRD.md via guided runtime session update Update existing PRD via guided runtime session validate Check PRD completeness (bash-only) status Quick PRD health check (one-liner) Coordinator (r0): coord Manual coordinator tools init Initialize a new mission mission Show mission progress dashboard status Check agent session health continue Generate continuation prompt run Generate context and launch selected runtime resume Crash recovery Options: -h, --help Show this help -v, --version Show version All arguments after the command are forwarded to the target CLI. USAGE } # Pre-flight checks check_mosaic_home() { if [[ ! -d "$MOSAIC_HOME" ]]; then echo "[mosaic] ERROR: ~/.config/mosaic not found." >&2 echo "[mosaic] Install with: curl -sL https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.sh | sh" >&2 exit 1 fi } check_agents_md() { if [[ ! -f "$MOSAIC_HOME/AGENTS.md" ]]; then echo "[mosaic] ERROR: ~/.config/mosaic/AGENTS.md not found." >&2 echo "[mosaic] Re-run the installer: cd ~/src/mosaic-bootstrap && bash install.sh" >&2 exit 1 fi } check_soul() { if [[ ! -f "$MOSAIC_HOME/SOUL.md" ]]; then echo "[mosaic] SOUL.md not found. Running mosaic init..." "$MOSAIC_HOME/bin/mosaic-init" fi } check_runtime() { local cmd="$1" if ! command -v "$cmd" >/dev/null 2>&1; then echo "[mosaic] ERROR: '$cmd' not found in PATH." >&2 echo "[mosaic] Install $cmd before launching." >&2 exit 1 fi } check_sequential_thinking() { local runtime="${1:-all}" local checker="$MOSAIC_HOME/bin/mosaic-ensure-sequential-thinking" if [[ ! -x "$checker" ]]; then echo "[mosaic] ERROR: sequential-thinking checker missing: $checker" >&2 exit 1 fi if ! "$checker" --check --runtime "$runtime" >/dev/null 2>&1; then echo "[mosaic] ERROR: sequential-thinking MCP is required but not configured." >&2 echo "[mosaic] Fix config: $checker --runtime $runtime" >&2 echo "[mosaic] Or run: mosaic seq fix --runtime $runtime" >&2 echo "[mosaic] Manual server start: mosaic seq start" >&2 exit 1 fi } runtime_contract_path() { local runtime="$1" case "$runtime" in claude) echo "$MOSAIC_HOME/runtime/claude/RUNTIME.md" ;; codex) echo "$MOSAIC_HOME/runtime/codex/RUNTIME.md" ;; opencode) echo "$MOSAIC_HOME/runtime/opencode/RUNTIME.md" ;; *) echo "[mosaic] ERROR: unsupported runtime '$runtime' for runtime contract." >&2 exit 1 ;; esac } build_runtime_prompt() { local runtime="$1" local runtime_file runtime_file="$(runtime_contract_path "$runtime")" if [[ ! -f "$runtime_file" ]]; then echo "[mosaic] ERROR: runtime contract not found: $runtime_file" >&2 exit 1 fi # Inject active mission context FIRST so the agent sees it immediately local mission_file=".mosaic/orchestrator/mission.json" if [[ -f "$mission_file" ]] && command -v jq &>/dev/null; then local m_status m_status="$(jq -r '.status // "inactive"' "$mission_file" 2>/dev/null)" if [[ "$m_status" == "active" || "$m_status" == "paused" ]]; then local m_name m_id m_count m_completed m_name="$(jq -r '.name // "unnamed"' "$mission_file")" m_id="$(jq -r '.mission_id // ""' "$mission_file")" m_count="$(jq '.milestones | length' "$mission_file")" m_completed="$(jq '[.milestones[] | select(.status == "completed")] | length' "$mission_file")" cat </dev/null && prd_sections=$((prd_sections + 1)) done prd_assumptions=$(grep -c 'ASSUMPTION:' "$prd_file" 2>/dev/null || echo 0) local prd_status="ready" (( prd_sections < 10 )) && prd_status="incomplete ($prd_sections/10 sections)" cat < "$tmp" if ! cmp -s "$tmp" "$dst" 2>/dev/null; then mv "$tmp" "$dst" else rm -f "$tmp" fi } # Detect active mission and return an initial prompt if one exists. # Sets MOSAIC_MISSION_PROMPT as a side effect. _detect_mission_prompt() { MOSAIC_MISSION_PROMPT="" local mission_file=".mosaic/orchestrator/mission.json" if [[ -f "$mission_file" ]] && command -v jq &>/dev/null; then local m_status m_status="$(jq -r '.status // "inactive"' "$mission_file" 2>/dev/null)" if [[ "$m_status" == "active" || "$m_status" == "paused" ]]; then local m_name m_name="$(jq -r '.name // "unnamed"' "$mission_file")" MOSAIC_MISSION_PROMPT="Active mission detected: ${m_name}. Read the mission state files and report status." fi fi } # Write a session lock if an active mission exists in the current directory. # Called before exec so $$ captures the PID that will become the agent process. _write_launcher_session_lock() { local runtime="$1" local mission_file=".mosaic/orchestrator/mission.json" local lock_file=".mosaic/orchestrator/session.lock" # Only write lock if mission exists and is active [[ -f "$mission_file" ]] || return 0 command -v jq &>/dev/null || return 0 local m_status m_status="$(jq -r '.status // "inactive"' "$mission_file" 2>/dev/null)" [[ "$m_status" == "active" || "$m_status" == "paused" ]] || return 0 local session_id session_id="${runtime}-$(date +%Y%m%d-%H%M%S)-$$" jq -n \ --arg sid "$session_id" \ --arg rt "$runtime" \ --arg pid "$$" \ --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ --arg pp "$(pwd)" \ --arg mid "" \ '{ session_id: $sid, runtime: $rt, pid: ($pid | tonumber), started_at: $ts, project_path: $pp, milestone_id: $mid }' > "$lock_file" } # Clean up session lock on exit (covers normal exit + signals). # Registered via trap after _write_launcher_session_lock succeeds. _cleanup_session_lock() { rm -f ".mosaic/orchestrator/session.lock" 2>/dev/null } # Launcher functions launch_claude() { check_mosaic_home check_agents_md check_soul check_runtime "claude" check_sequential_thinking "claude" _check_resumable_session # Claude supports --append-system-prompt for direct injection local runtime_prompt runtime_prompt="$(build_runtime_prompt "claude")" # If active mission exists and no user prompt was given, inject initial prompt _detect_mission_prompt _write_launcher_session_lock "claude" trap _cleanup_session_lock EXIT INT TERM if [[ -n "$MOSAIC_MISSION_PROMPT" && $# -eq 0 ]]; then echo "[mosaic] Launching Claude Code (active mission detected)..." exec claude --append-system-prompt "$runtime_prompt" "$MOSAIC_MISSION_PROMPT" else echo "[mosaic] Launching Claude Code..." exec claude --append-system-prompt "$runtime_prompt" "$@" fi } launch_opencode() { check_mosaic_home check_agents_md check_soul check_runtime "opencode" check_sequential_thinking "opencode" _check_resumable_session # OpenCode reads from ~/.config/opencode/AGENTS.md ensure_runtime_config "opencode" "$HOME/.config/opencode/AGENTS.md" _write_launcher_session_lock "opencode" trap _cleanup_session_lock EXIT INT TERM echo "[mosaic] Launching OpenCode..." exec opencode "$@" } launch_codex() { check_mosaic_home check_agents_md check_soul check_runtime "codex" check_sequential_thinking "codex" _check_resumable_session # Codex reads from ~/.codex/instructions.md ensure_runtime_config "codex" "$HOME/.codex/instructions.md" _detect_mission_prompt _write_launcher_session_lock "codex" trap _cleanup_session_lock EXIT INT TERM if [[ -n "$MOSAIC_MISSION_PROMPT" && $# -eq 0 ]]; then echo "[mosaic] Launching Codex (active mission detected)..." exec codex "$MOSAIC_MISSION_PROMPT" else echo "[mosaic] Launching Codex..." exec codex "$@" fi } launch_yolo() { if [[ $# -eq 0 ]]; then echo "[mosaic] ERROR: yolo requires a runtime (claude|codex|opencode)." >&2 echo "[mosaic] Example: mosaic yolo claude" >&2 exit 1 fi local runtime="$1" shift case "$runtime" in claude) check_mosaic_home check_agents_md check_soul check_runtime "claude" check_sequential_thinking "claude" # Claude uses an explicit dangerous permissions flag. local runtime_prompt runtime_prompt="$(build_runtime_prompt "claude")" _detect_mission_prompt _write_launcher_session_lock "claude" trap _cleanup_session_lock EXIT INT TERM if [[ -n "$MOSAIC_MISSION_PROMPT" && $# -eq 0 ]]; then echo "[mosaic] Launching Claude Code in YOLO mode (active mission detected)..." exec claude --dangerously-skip-permissions --append-system-prompt "$runtime_prompt" "$MOSAIC_MISSION_PROMPT" else echo "[mosaic] Launching Claude Code in YOLO mode (dangerous permissions enabled)..." exec claude --dangerously-skip-permissions --append-system-prompt "$runtime_prompt" "$@" fi ;; codex) check_mosaic_home check_agents_md check_soul check_runtime "codex" check_sequential_thinking "codex" # Codex reads instructions.md from ~/.codex and supports a direct dangerous flag. ensure_runtime_config "codex" "$HOME/.codex/instructions.md" _detect_mission_prompt _write_launcher_session_lock "codex" trap _cleanup_session_lock EXIT INT TERM if [[ -n "$MOSAIC_MISSION_PROMPT" && $# -eq 0 ]]; then echo "[mosaic] Launching Codex in YOLO mode (active mission detected)..." exec codex --dangerously-bypass-approvals-and-sandbox "$MOSAIC_MISSION_PROMPT" else echo "[mosaic] Launching Codex in YOLO mode (dangerous permissions enabled)..." exec codex --dangerously-bypass-approvals-and-sandbox "$@" fi ;; opencode) check_mosaic_home check_agents_md check_soul check_runtime "opencode" check_sequential_thinking "opencode" # OpenCode defaults to allow-all permissions unless user config restricts them. ensure_runtime_config "opencode" "$HOME/.config/opencode/AGENTS.md" _write_launcher_session_lock "opencode" trap _cleanup_session_lock EXIT INT TERM echo "[mosaic] Launching OpenCode in YOLO mode..." exec opencode "$@" ;; *) echo "[mosaic] ERROR: Unsupported yolo runtime '$runtime'. Use claude|codex|opencode." >&2 exit 1 ;; esac } # Delegate to existing scripts run_init() { # Prefer wizard if Node.js and bundle are available local wizard_bin="$MOSAIC_HOME/dist/mosaic-wizard.mjs" if command -v node >/dev/null 2>&1 && [[ -f "$wizard_bin" ]]; then exec node "$wizard_bin" "$@" fi # Fallback to legacy bash wizard check_mosaic_home exec "$MOSAIC_HOME/bin/mosaic-init" "$@" } run_doctor() { check_mosaic_home exec "$MOSAIC_HOME/bin/mosaic-doctor" "$@" } run_sync() { check_mosaic_home exec "$MOSAIC_HOME/bin/mosaic-sync-skills" "$@" } run_macp() { check_mosaic_home exec "$MOSAIC_HOME/bin/mosaic-macp" "$@" } run_seq() { check_mosaic_home local checker="$MOSAIC_HOME/bin/mosaic-ensure-sequential-thinking" local action="${1:-check}" case "$action" in check) shift || true exec "$checker" --check "$@" ;; fix|apply) shift || true exec "$checker" "$@" ;; start) shift || true check_runtime "npx" echo "[mosaic] Starting sequential-thinking MCP server..." exec npx -y @modelcontextprotocol/server-sequential-thinking "$@" ;; *) echo "[mosaic] ERROR: Unknown seq subcommand '$action'." >&2 echo "[mosaic] Use: mosaic seq check|fix|start" >&2 exit 1 ;; esac } run_coord() { check_mosaic_home local runtime="claude" local runtime_flag="" local -a coord_args=() while [[ $# -gt 0 ]]; do case "$1" in --claude|--codex) local selected_runtime="${1#--}" if [[ -n "$runtime_flag" ]] && [[ "$runtime" != "$selected_runtime" ]]; then echo "[mosaic] ERROR: --claude and --codex are mutually exclusive for 'mosaic coord'." >&2 exit 1 fi runtime="$selected_runtime" runtime_flag="$1" shift ;; *) coord_args+=("$1") shift ;; esac done local subcmd="${coord_args[0]:-help}" if (( ${#coord_args[@]} > 1 )); then set -- "${coord_args[@]:1}" else set -- fi local tool_dir="$MOSAIC_HOME/tools/orchestrator" case "$subcmd" in status|session) MOSAIC_COORD_RUNTIME="$runtime" exec bash "$tool_dir/session-status.sh" "$@" ;; init) MOSAIC_COORD_RUNTIME="$runtime" exec bash "$tool_dir/mission-init.sh" "$@" ;; mission|progress) MOSAIC_COORD_RUNTIME="$runtime" exec bash "$tool_dir/mission-status.sh" "$@" ;; continue|next) MOSAIC_COORD_RUNTIME="$runtime" exec bash "$tool_dir/continue-prompt.sh" "$@" ;; run|start) MOSAIC_COORD_RUNTIME="$runtime" exec bash "$tool_dir/session-run.sh" "$@" ;; smoke|test) MOSAIC_COORD_RUNTIME="$runtime" exec bash "$tool_dir/smoke-test.sh" "$@" ;; resume|recover) MOSAIC_COORD_RUNTIME="$runtime" exec bash "$tool_dir/session-resume.sh" "$@" ;; help|*) cat < [opts] Initialize a new mission mission [--project ] Show mission progress dashboard status [--project ] Check agent session health continue [--project ] Generate continuation prompt for next session run [--project ] Generate context and launch selected runtime smoke Run orchestration behavior smoke checks resume [--project ] Crash recovery (detect dirty state, generate fix) Runtime: --claude Use Claude runtime hints/prompts (default) --codex Use Codex runtime hints/prompts Examples: mosaic coord init --name "Security Fix" --milestones "Critical,High,Medium" mosaic coord mission mosaic coord --codex mission mosaic coord continue --copy mosaic coord run mosaic coord run --codex mosaic coord smoke mosaic coord continue --codex --copy COORD_USAGE ;; esac } # Resume advisory — prints warning if active mission or stale session detected _check_resumable_session() { local mission_file=".mosaic/orchestrator/mission.json" local lock_file=".mosaic/orchestrator/session.lock" command -v jq &>/dev/null || return 0 if [[ -f "$lock_file" ]]; then local pid pid="$(jq -r '.pid // 0' "$lock_file" 2>/dev/null)" if [[ -n "$pid" ]] && [[ "$pid" != "0" ]] && ! kill -0 "$pid" 2>/dev/null; then # Stale lock from a dead session — clean it up rm -f "$lock_file" echo "[mosaic] Cleaned up stale session lock (PID $pid no longer running)." echo "" fi elif [[ -f "$mission_file" ]]; then local status status="$(jq -r '.status // "inactive"' "$mission_file" 2>/dev/null)" if [[ "$status" == "active" ]]; then echo "[mosaic] Active mission detected. Generate continuation prompt with:" echo "[mosaic] mosaic coord continue" echo "" fi fi } run_prdy() { check_mosaic_home local runtime="claude" local runtime_flag="" local -a prdy_args=() while [[ $# -gt 0 ]]; do case "$1" in --claude|--codex) local selected_runtime="${1#--}" if [[ -n "$runtime_flag" ]] && [[ "$runtime" != "$selected_runtime" ]]; then echo "[mosaic] ERROR: --claude and --codex are mutually exclusive for 'mosaic prdy'." >&2 exit 1 fi runtime="$selected_runtime" runtime_flag="$1" shift ;; *) prdy_args+=("$1") shift ;; esac done local subcmd="${prdy_args[0]:-help}" if (( ${#prdy_args[@]} > 1 )); then set -- "${prdy_args[@]:1}" else set -- fi local tool_dir="$MOSAIC_HOME/tools/prdy" case "$subcmd" in init) MOSAIC_PRDY_RUNTIME="$runtime" exec bash "$tool_dir/prdy-init.sh" "$@" ;; update) MOSAIC_PRDY_RUNTIME="$runtime" exec bash "$tool_dir/prdy-update.sh" "$@" ;; validate|check) MOSAIC_PRDY_RUNTIME="$runtime" exec bash "$tool_dir/prdy-validate.sh" "$@" ;; status) exec bash "$tool_dir/prdy-status.sh" "$@" ;; help|*) cat <] [--name ] Create docs/PRD.md via guided runtime session update [--project ] Update existing docs/PRD.md via guided runtime session validate [--project ] Check PRD completeness against Mosaic guide (bash-only) status [--project ] [--format short|json] Quick PRD health check (one-liner) Runtime: --claude Use Claude runtime (default) --codex Use Codex runtime Examples: mosaic prdy init --name "User Authentication" mosaic prdy update mosaic prdy --codex init --name "User Authentication" mosaic prdy validate Output location: docs/PRD.md (per Mosaic PRD guide) PRDY_USAGE ;; esac } run_bootstrap() { check_mosaic_home exec "$MOSAIC_HOME/bin/mosaic-bootstrap-repo" "$@" } run_release_upgrade() { check_mosaic_home exec "$MOSAIC_HOME/bin/mosaic-release-upgrade" "$@" } run_project_upgrade() { check_mosaic_home exec "$MOSAIC_HOME/bin/mosaic-upgrade" "$@" } run_upgrade() { check_mosaic_home # Default: upgrade installed release if [[ $# -eq 0 ]]; then run_release_upgrade fi case "$1" in release) shift run_release_upgrade "$@" ;; check) shift run_release_upgrade --dry-run "$@" ;; project) shift run_project_upgrade "$@" ;; # Backward compatibility for historical project-upgrade usage. --all|--root) run_project_upgrade "$@" ;; --dry-run|--ref|--keep|--overwrite|-y|--yes) run_release_upgrade "$@" ;; -*) run_release_upgrade "$@" ;; *) run_project_upgrade "$@" ;; esac } # Main router if [[ $# -eq 0 ]]; then usage exit 0 fi command="$1" shift case "$command" in claude) launch_claude "$@" ;; opencode) launch_opencode "$@" ;; codex) launch_codex "$@" ;; yolo|--yolo) launch_yolo "$@" ;; init) run_init "$@" ;; doctor) run_doctor "$@" ;; sync) run_sync "$@" ;; macp) run_macp "$@" ;; seq) run_seq "$@" ;; bootstrap) run_bootstrap "$@" ;; prdy) run_prdy "$@" ;; coord) run_coord "$@" ;; upgrade) run_upgrade "$@" ;; release-upgrade) run_release_upgrade "$@" ;; project-upgrade) run_project_upgrade "$@" ;; help|-h|--help) usage ;; version|-v|--version) echo "mosaic $VERSION" ;; *) echo "[mosaic] Unknown command: $command" >&2 echo "[mosaic] Run 'mosaic --help' for usage." >&2 exit 1 ;; esac