diff --git a/bin/mosaic b/bin/mosaic index 4afdedc..9a773f9 100755 --- a/bin/mosaic +++ b/bin/mosaic @@ -247,6 +247,47 @@ _detect_mission_prompt() { 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 @@ -263,6 +304,8 @@ launch_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" @@ -283,6 +326,8 @@ launch_opencode() { # 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 "$@" } @@ -298,6 +343,8 @@ launch_codex() { # Codex reads from ~/.codex/instructions.md ensure_runtime_config "codex" "$HOME/.codex/instructions.md" + _write_launcher_session_lock "codex" + trap _cleanup_session_lock EXIT INT TERM echo "[mosaic] Launching Codex..." exec codex "$@" } @@ -325,6 +372,8 @@ launch_yolo() { 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" @@ -342,6 +391,8 @@ launch_yolo() { # Codex reads instructions.md from ~/.codex and supports a direct dangerous flag. ensure_runtime_config "codex" "$HOME/.codex/instructions.md" + _write_launcher_session_lock "codex" + trap _cleanup_session_lock EXIT INT TERM echo "[mosaic] Launching Codex in YOLO mode (dangerous permissions enabled)..." exec codex --dangerously-bypass-approvals-and-sandbox "$@" ;; @@ -354,6 +405,8 @@ launch_yolo() { # 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 "$@" ;; @@ -469,8 +522,9 @@ _check_resumable_session() { local pid pid="$(jq -r '.pid // 0' "$lock_file" 2>/dev/null)" if [[ -n "$pid" ]] && [[ "$pid" != "0" ]] && ! kill -0 "$pid" 2>/dev/null; then - echo "[mosaic] Previous orchestration session detected (crashed)." - echo "[mosaic] Run: mosaic coord resume" + # 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