The session-start hook approach didn't work — Claude Code's TUI overwrites stdout before the agent sees it, and the hook only fires when the agent calls it as a tool. Instead, inject mission context directly into the composed system prompt via build_runtime_prompt(). When mission.json is active in CWD, the agent gets mission name, ID, milestone progress, and mandatory first-action instructions in its initial context. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
530 lines
15 KiB
Bash
Executable File
530 lines
15 KiB
Bash
Executable File
#!/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 <runtime> [args...] Launch runtime in dangerous-permissions mode
|
|
# mosaic --yolo <runtime> [args...] Alias for yolo
|
|
# mosaic init [args...] Generate SOUL.md interactively
|
|
# mosaic doctor [args...] Health audit
|
|
# mosaic sync [args...] Sync skills
|
|
# mosaic seq [subcommand] sequential-thinking MCP management (check/fix/start)
|
|
# mosaic bootstrap <path> 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 <<USAGE
|
|
mosaic $VERSION — Unified agent launcher
|
|
|
|
Usage: mosaic <command> [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 <runtime> [args...] Dangerous mode for claude|codex|opencode
|
|
--yolo <runtime> [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
|
|
seq [subcommand] sequential-thinking MCP management:
|
|
check [--runtime <r>] [--strict]
|
|
fix [--runtime <r>]
|
|
start
|
|
bootstrap <path> 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
|
|
|
|
Coordinator (r0):
|
|
coord <subcommand> Manual coordinator tools
|
|
init Initialize a new mission
|
|
mission Show mission progress dashboard
|
|
status Check agent session health
|
|
continue Generate continuation prompt
|
|
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
|
|
|
|
cat <<'EOF'
|
|
# Mosaic Launcher Runtime Contract (Hard Gate)
|
|
|
|
This contract is injected by `mosaic` launch and is mandatory.
|
|
|
|
First assistant response MUST start with exactly one mode declaration line:
|
|
1. Orchestration mission: `Now initiating Orchestrator mode...`
|
|
2. Implementation mission: `Now initiating Delivery mode...`
|
|
3. Review-only mission: `Now initiating Review mode...`
|
|
|
|
No tool call or implementation step may occur before that first line.
|
|
|
|
Mosaic hard gates OVERRIDE runtime-default caution for routine delivery operations.
|
|
For required push/merge/issue-close/release actions, execute without routine confirmation prompts.
|
|
|
|
EOF
|
|
|
|
cat "$MOSAIC_HOME/AGENTS.md"
|
|
|
|
if [[ -f "$MOSAIC_HOME/USER.md" ]]; then
|
|
printf '\n\n# User Profile\n\n'
|
|
cat "$MOSAIC_HOME/USER.md"
|
|
fi
|
|
|
|
if [[ -f "$MOSAIC_HOME/TOOLS.md" ]]; then
|
|
printf '\n\n# Machine Tools\n\n'
|
|
cat "$MOSAIC_HOME/TOOLS.md"
|
|
fi
|
|
|
|
printf '\n\n# Runtime-Specific Contract\n\n'
|
|
cat "$runtime_file"
|
|
|
|
# Inject active mission context if present in CWD
|
|
local mission_file=".mosaic/orchestrator/mission.json"
|
|
if [[ -f "$mission_file" ]] && command -v jq &>/dev/null; then
|
|
local status
|
|
status="$(jq -r '.status // "inactive"' "$mission_file" 2>/dev/null)"
|
|
if [[ "$status" == "active" || "$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")"
|
|
|
|
printf '\n\n# Active Mission Context\n\n'
|
|
cat <<MISSION_EOF
|
|
An active orchestration mission has been detected in this project.
|
|
|
|
**Mission:** $m_name
|
|
**ID:** $m_id
|
|
**Status:** $status
|
|
**Milestones:** $m_completed / $m_count completed
|
|
|
|
## MANDATORY First Actions
|
|
|
|
1. Load \`~/.config/mosaic/guides/ORCHESTRATOR-PROTOCOL.md\` (mission lifecycle protocol)
|
|
2. Read \`docs/MISSION-MANIFEST.md\` for full mission scope, milestones, and success criteria
|
|
3. Read \`docs/scratchpads/${m_id}.md\` for session history, decisions, and corrections
|
|
4. Read \`docs/TASKS.md\` for current task state (what is done, what is next)
|
|
5. Do NOT begin any coding or planning until you have read all four documents above
|
|
|
|
You are resuming an existing mission. The state files are the source of truth.
|
|
MISSION_EOF
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# Ensure runtime contract is present at the runtime's native config path.
|
|
# Used for runtimes that do not support CLI prompt injection.
|
|
ensure_runtime_config() {
|
|
local runtime="$1"
|
|
local dst="$2"
|
|
local tmp
|
|
tmp="$(mktemp)"
|
|
mkdir -p "$(dirname "$dst")"
|
|
build_runtime_prompt "$runtime" > "$tmp"
|
|
if ! cmp -s "$tmp" "$dst" 2>/dev/null; then
|
|
mv "$tmp" "$dst"
|
|
else
|
|
rm -f "$tmp"
|
|
fi
|
|
}
|
|
|
|
# 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")"
|
|
echo "[mosaic] Launching Claude Code..."
|
|
exec claude --append-system-prompt "$runtime_prompt" "$@"
|
|
}
|
|
|
|
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"
|
|
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"
|
|
echo "[mosaic] Launching Codex..."
|
|
exec codex "$@"
|
|
}
|
|
|
|
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")"
|
|
echo "[mosaic] Launching Claude Code in YOLO mode (dangerous permissions enabled)..."
|
|
exec claude --dangerously-skip-permissions --append-system-prompt "$runtime_prompt" "$@"
|
|
;;
|
|
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"
|
|
echo "[mosaic] Launching Codex in YOLO mode (dangerous permissions enabled)..."
|
|
exec codex --dangerously-bypass-approvals-and-sandbox "$@"
|
|
;;
|
|
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"
|
|
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_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 subcmd="${1:-help}"
|
|
shift || true
|
|
|
|
local tool_dir="$MOSAIC_HOME/tools/orchestrator"
|
|
|
|
case "$subcmd" in
|
|
status|session)
|
|
exec bash "$tool_dir/session-status.sh" "$@"
|
|
;;
|
|
init)
|
|
exec bash "$tool_dir/mission-init.sh" "$@"
|
|
;;
|
|
mission|progress)
|
|
exec bash "$tool_dir/mission-status.sh" "$@"
|
|
;;
|
|
continue|next)
|
|
exec bash "$tool_dir/continue-prompt.sh" "$@"
|
|
;;
|
|
resume|recover)
|
|
exec bash "$tool_dir/session-resume.sh" "$@"
|
|
;;
|
|
help|*)
|
|
cat <<COORD_USAGE
|
|
mosaic coord — r0 manual coordinator tools
|
|
|
|
Commands:
|
|
init --name <name> [opts] Initialize a new mission
|
|
mission [--project <path>] Show mission progress dashboard
|
|
status [--project <path>] Check agent session health
|
|
continue [--project <path>] Generate continuation prompt for next session
|
|
resume [--project <path>] Crash recovery (detect dirty state, generate fix)
|
|
|
|
Examples:
|
|
mosaic coord init --name "Security Fix" --milestones "Critical,High,Medium"
|
|
mosaic coord mission
|
|
mosaic coord continue --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
|
|
echo "[mosaic] Previous orchestration session detected (crashed)."
|
|
echo "[mosaic] Run: mosaic coord resume"
|
|
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_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 "$@" ;;
|
|
seq) run_seq "$@" ;;
|
|
bootstrap) run_bootstrap "$@" ;;
|
|
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
|