feat: integrate framework files into monorepo under packages/mosaic/framework/
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful

Moves all Mosaic framework runtime files from the separate bootstrap repo
into the monorepo as canonical source. The @mosaic/mosaic npm package now
ships the complete framework — bin scripts, runtime configs, tools, and
templates — enabling standalone installation via npm install.

Structure:
  packages/mosaic/framework/
  ├── bin/          28 CLI scripts (mosaic, mosaic-doctor, mosaic-sync-skills, etc.)
  ├── runtime/      Runtime adapters (claude, codex, opencode, pi, mcp)
  ├── tools/        Shell tooling (git, prdy, orchestrator, quality, etc.)
  ├── templates/    Agent and repo templates
  ├── defaults/     Default identity files (AGENTS.md, STANDARDS.md, SOUL.md, etc.)
  ├── install.sh    Legacy bash installer
  └── remote-install.sh  One-liner remote installer

Key files with Pi support and recent fixes:
- bin/mosaic: launch_pi() with skills-local loop
- bin/mosaic-doctor: --fix auto-wiring for all 4 harnesses
- bin/mosaic-sync-skills: Pi as 4th link target, symlink-aware find
- bin/mosaic-link-runtime-assets: Pi settings.json patching
- bin/mosaic-migrate-local-skills: Pi skill roots, symlink find
- runtime/pi/RUNTIME.md + mosaic-extension.ts

Package ships 251 framework files in the npm tarball (278KB compressed).
This commit is contained in:
Jason Woltje
2026-04-01 21:19:21 -05:00
parent f3cb3e6852
commit b38cfac760
252 changed files with 31477 additions and 1 deletions

View File

@@ -0,0 +1,849 @@
#!/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:
pi [args...] Launch Pi with runtime contract injected (recommended)
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|pi
--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
PRD:
prdy <subcommand> 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 <subcommand> 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" ;;
pi) echo "$MOSAIC_HOME/runtime/pi/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 <<MISSION_EOF
# ACTIVE MISSION — HARD GATE (Read Before Anything Else)
An active orchestration mission exists in this project. This is a BLOCKING requirement.
**Mission:** $m_name
**ID:** $m_id
**Status:** $m_status
**Milestones:** $m_completed / $m_count completed
## MANDATORY — Before ANY Response to the User
You MUST complete these steps before responding to any user message, including simple greetings:
1. Read \`~/.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 the latest scratchpad in \`docs/scratchpads/\` for session history, decisions, and corrections
4. Read \`docs/TASKS.md\` for current task state (what is done, what is next)
5. After reading all four, acknowledge the mission state to the user before proceeding
If the user gives a task, execute it within the mission context. If no task is given, present mission status and ask how to proceed.
MISSION_EOF
fi
fi
# Inject PRD status so the agent knows requirements state
local prd_file="docs/PRD.md"
if [[ -f "$prd_file" ]]; then
local prd_sections=0
local prd_assumptions=0
for entry in "Problem Statement|^#{2,3} .*(problem statement|objective)" \
"Scope / Non-Goals|^#{2,3} .*(scope|non.goal|out of scope|in.scope)" \
"User Stories / Requirements|^#{2,3} .*(user stor|stakeholder|user.*requirement)" \
"Functional Requirements|^#{2,3} .*functional requirement" \
"Non-Functional Requirements|^#{2,3} .*non.functional" \
"Acceptance Criteria|^#{2,3} .*acceptance criteria" \
"Technical Considerations|^#{2,3} .*(technical consideration|constraint|dependenc)" \
"Risks / Open Questions|^#{2,3} .*(risk|open question)" \
"Success Metrics / Testing|^#{2,3} .*(success metric|test|verification)" \
"Milestones / Delivery|^#{2,3} .*(milestone|delivery|scope version)"; do
local pattern="${entry#*|}"
grep -qiE "$pattern" "$prd_file" 2>/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 <<PRD_EOF
# PRD Status
- **File:** docs/PRD.md
- **Status:** $prd_status
- **Assumptions:** $prd_assumptions
PRD_EOF
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"
}
# 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
}
# 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_pi() {
check_mosaic_home
check_agents_md
check_soul
check_runtime "pi"
# Pi has native thinking levels — no sequential-thinking gate required
_check_resumable_session
local runtime_prompt
runtime_prompt="$(build_runtime_prompt "pi")"
# Build skill args from Mosaic skills directories (canonical + local)
local -a skill_args=()
for skills_root in "$MOSAIC_HOME/skills" "$MOSAIC_HOME/skills-local"; do
[[ -d "$skills_root" ]] || continue
for skill_dir in "$skills_root"/*/; do
[[ -f "${skill_dir}SKILL.md" ]] && skill_args+=(--skill "$skill_dir")
done
done
# Load Mosaic extension if present
local -a ext_args=()
local mosaic_ext="$MOSAIC_HOME/runtime/pi/mosaic-extension.ts"
[[ -f "$mosaic_ext" ]] && ext_args=(--extension "$mosaic_ext")
_detect_mission_prompt
_write_launcher_session_lock "pi"
trap _cleanup_session_lock EXIT INT TERM
if [[ -n "$MOSAIC_MISSION_PROMPT" && $# -eq 0 ]]; then
echo "[mosaic] Launching Pi (active mission detected)..."
exec pi --append-system-prompt "$runtime_prompt" \
"${skill_args[@]}" "${ext_args[@]}" "$MOSAIC_MISSION_PROMPT"
else
echo "[mosaic] Launching Pi..."
exec pi --append-system-prompt "$runtime_prompt" \
"${skill_args[@]}" "${ext_args[@]}" "$@"
fi
}
launch_yolo() {
if [[ $# -eq 0 ]]; then
echo "[mosaic] ERROR: yolo requires a runtime (claude|codex|opencode|pi)." >&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 "$@"
;;
pi)
# Pi has no permission restrictions — yolo is identical to normal launch
launch_pi "$@"
;;
*)
echo "[mosaic] ERROR: Unsupported yolo runtime '$runtime'. Use claude|codex|opencode|pi." >&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 runtime="claude"
local runtime_flag=""
local yolo_flag=""
local -a coord_args=()
while [[ $# -gt 0 ]]; do
case "$1" in
--claude|--codex|--pi)
local selected_runtime="${1#--}"
if [[ -n "$runtime_flag" ]] && [[ "$runtime" != "$selected_runtime" ]]; then
echo "[mosaic] ERROR: --claude, --codex, and --pi are mutually exclusive for 'mosaic coord'." >&2
exit 1
fi
runtime="$selected_runtime"
runtime_flag="$1"
shift
;;
--yolo)
yolo_flag="--yolo"
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" ${yolo_flag:+"$yolo_flag"} "$@"
;;
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 <<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
run [--project <path>] Generate context and launch selected runtime
smoke Run orchestration behavior smoke checks
resume [--project <path>] Crash recovery (detect dirty state, generate fix)
Runtime:
--claude Use Claude runtime hints/prompts (default)
--codex Use Codex runtime hints/prompts
--pi Use Pi runtime hints/prompts
--yolo Launch runtime in dangerous/skip-permissions mode (run only)
Examples:
mosaic coord init --name "Security Fix" --milestones "Critical,High,Medium"
mosaic coord mission
mosaic coord --codex mission
mosaic coord --pi run
mosaic coord continue --copy
mosaic coord run
mosaic coord run --codex
mosaic coord --yolo run
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|--pi)
local selected_runtime="${1#--}"
if [[ -n "$runtime_flag" ]] && [[ "$runtime" != "$selected_runtime" ]]; then
echo "[mosaic] ERROR: --claude, --codex, and --pi 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 <<PRDY_USAGE
mosaic prdy — PRD creation and validation tools
Commands:
init [--project <path>] [--name <feature>] Create docs/PRD.md via guided runtime session
update [--project <path>] Update existing docs/PRD.md via guided runtime session
validate [--project <path>] Check PRD completeness against Mosaic guide (bash-only)
status [--project <path>] [--format short|json] Quick PRD health check (one-liner)
Runtime:
--claude Use Claude runtime (default)
--codex Use Codex runtime
--pi Use Pi runtime
Examples:
mosaic prdy init --name "User Authentication"
mosaic prdy update
mosaic prdy --pi init --name "User Authentication"
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
pi) launch_pi "$@" ;;
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 "$@" ;;
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

View File

@@ -0,0 +1,126 @@
#!/usr/bin/env bash
set -euo pipefail
TARGET_DIR="$(pwd)"
FORCE=0
QUALITY_TEMPLATE=""
while [[ $# -gt 0 ]]; do
case "$1" in
--force)
FORCE=1
shift
;;
--quality-template)
QUALITY_TEMPLATE="${2:-}"
shift 2
;;
*)
TARGET_DIR="$1"
shift
;;
esac
done
if [[ ! -d "$TARGET_DIR" ]]; then
echo "[mosaic] Target directory does not exist: $TARGET_DIR" >&2
exit 1
fi
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
TEMPLATE_ROOT="$MOSAIC_HOME/templates/repo"
if [[ ! -d "$TEMPLATE_ROOT" ]]; then
echo "[mosaic] Missing templates at $TEMPLATE_ROOT" >&2
echo "[mosaic] Install or refresh framework: ~/.config/mosaic/install.sh" >&2
exit 1
fi
mkdir -p "$TARGET_DIR/.mosaic" "$TARGET_DIR/scripts/agent"
mkdir -p "$TARGET_DIR/.mosaic/orchestrator" "$TARGET_DIR/.mosaic/orchestrator/logs" "$TARGET_DIR/.mosaic/orchestrator/results"
copy_file() {
local src="$1"
local dst="$2"
if [[ -f "$dst" && "$FORCE" -ne 1 ]]; then
echo "[mosaic] Skip existing: $dst"
return
fi
cp "$src" "$dst"
echo "[mosaic] Wrote: $dst"
}
copy_file "$TEMPLATE_ROOT/.mosaic/README.md" "$TARGET_DIR/.mosaic/README.md"
copy_file "$TEMPLATE_ROOT/.mosaic/repo-hooks.sh" "$TARGET_DIR/.mosaic/repo-hooks.sh"
copy_file "$TEMPLATE_ROOT/.mosaic/quality-rails.yml" "$TARGET_DIR/.mosaic/quality-rails.yml"
copy_file "$TEMPLATE_ROOT/.mosaic/orchestrator/config.json" "$TARGET_DIR/.mosaic/orchestrator/config.json"
copy_file "$TEMPLATE_ROOT/.mosaic/orchestrator/tasks.json" "$TARGET_DIR/.mosaic/orchestrator/tasks.json"
copy_file "$TEMPLATE_ROOT/.mosaic/orchestrator/state.json" "$TARGET_DIR/.mosaic/orchestrator/state.json"
copy_file "$TEMPLATE_ROOT/.mosaic/orchestrator/matrix_state.json" "$TARGET_DIR/.mosaic/orchestrator/matrix_state.json"
copy_file "$TEMPLATE_ROOT/.mosaic/orchestrator/logs/.gitkeep" "$TARGET_DIR/.mosaic/orchestrator/logs/.gitkeep"
copy_file "$TEMPLATE_ROOT/.mosaic/orchestrator/results/.gitkeep" "$TARGET_DIR/.mosaic/orchestrator/results/.gitkeep"
for file in "$TEMPLATE_ROOT"/scripts/agent/*.sh; do
base="$(basename "$file")"
copy_file "$file" "$TARGET_DIR/scripts/agent/$base"
chmod +x "$TARGET_DIR/scripts/agent/$base"
done
if [[ ! -f "$TARGET_DIR/AGENTS.md" ]]; then
cat > "$TARGET_DIR/AGENTS.md" <<'AGENTS_EOF'
# Agent Guidelines
## Required Load Order
1. `~/.config/mosaic/SOUL.md`
2. `~/.config/mosaic/STANDARDS.md`
3. `~/.config/mosaic/AGENTS.md`
4. `~/.config/mosaic/guides/E2E-DELIVERY.md`
5. `AGENTS.md` (this file)
6. Runtime-specific guide: `~/.config/mosaic/runtime/<runtime>/RUNTIME.md`
7. `.mosaic/repo-hooks.sh`
## Session Lifecycle
```bash
bash scripts/agent/session-start.sh
bash scripts/agent/critical.sh
bash scripts/agent/session-end.sh
```
## Shared Tools
- Quality and orchestration guides: `~/.config/mosaic/guides/`
- Shared automation tools: `~/.config/mosaic/tools/`
## Repo-Specific Notes
- Add project constraints and workflows here.
- Implement hook functions in `.mosaic/repo-hooks.sh`.
- Scratchpads are mandatory for non-trivial tasks.
AGENTS_EOF
echo "[mosaic] Wrote: $TARGET_DIR/AGENTS.md"
else
echo "[mosaic] AGENTS.md exists; add standards load order if missing"
fi
echo "[mosaic] Repo bootstrap complete: $TARGET_DIR"
echo "[mosaic] Next: edit $TARGET_DIR/.mosaic/repo-hooks.sh with project workflows"
echo "[mosaic] Optional: apply quality tools via ~/.config/mosaic/bin/mosaic-quality-apply --template <template> --target $TARGET_DIR"
echo "[mosaic] Optional: run orchestrator rail via ~/.config/mosaic/bin/mosaic-orchestrator-drain"
echo "[mosaic] Optional: run detached orchestrator via bash $TARGET_DIR/scripts/agent/orchestrator-daemon.sh start"
if [[ -n "$QUALITY_TEMPLATE" ]]; then
if [[ -x "$MOSAIC_HOME/bin/mosaic-quality-apply" ]]; then
"$MOSAIC_HOME/bin/mosaic-quality-apply" --template "$QUALITY_TEMPLATE" --target "$TARGET_DIR"
if [[ -f "$TARGET_DIR/.mosaic/quality-rails.yml" ]]; then
sed -i "s/^enabled:.*/enabled: true/" "$TARGET_DIR/.mosaic/quality-rails.yml"
sed -i "s/^template:.*/template: \"$QUALITY_TEMPLATE\"/" "$TARGET_DIR/.mosaic/quality-rails.yml"
fi
echo "[mosaic] Applied quality tools template: $QUALITY_TEMPLATE"
else
echo "[mosaic] WARN: mosaic-quality-apply not found; skipping quality tools apply" >&2
fi
fi

View File

@@ -0,0 +1,147 @@
#!/usr/bin/env bash
set -euo pipefail
RUNTIME="claude"
APPLY=0
ALL_EMPTY=0
usage() {
cat <<USAGE
Usage: $(basename "$0") [options]
Remove empty runtime directories created by migration/drift.
Default mode only checks managed legacy surfaces. Use --all-empty for broader cleanup.
Options:
--runtime <name> Runtime to clean (default: claude)
--all-empty Scan all runtime directories (except protected paths)
--apply Perform deletions (default: dry-run)
-h, --help Show help
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--runtime)
[[ $# -lt 2 ]] && { echo "Missing value for --runtime" >&2; exit 1; }
RUNTIME="$2"
shift 2
;;
--all-empty)
ALL_EMPTY=1
shift
;;
--apply)
APPLY=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
case "$RUNTIME" in
claude)
TARGET_ROOT="$HOME/.claude"
managed_roots=(
"$HOME/.claude/agent-guides"
"$HOME/.claude/scripts"
"$HOME/.claude/templates"
"$HOME/.claude/presets"
"$HOME/.claude/skills"
"$HOME/.claude/agents"
"$HOME/.claude/agents.bak"
)
protected_roots=(
"$HOME/.claude/.git"
"$HOME/.claude/debug"
"$HOME/.claude/file-history"
"$HOME/.claude/projects"
"$HOME/.claude/session-env"
"$HOME/.claude/tasks"
"$HOME/.claude/todos"
"$HOME/.claude/plugins"
"$HOME/.claude/statsig"
"$HOME/.claude/logs"
"$HOME/.claude/shell-snapshots"
"$HOME/.claude/paste-cache"
"$HOME/.claude/plans"
"$HOME/.claude/ide"
"$HOME/.claude/cache"
)
;;
*)
echo "Unsupported runtime: $RUNTIME" >&2
exit 1
;;
esac
[[ -d "$TARGET_ROOT" ]] || { echo "[mosaic-clean] Runtime dir missing: $TARGET_ROOT" >&2; exit 1; }
is_protected() {
local path="$1"
for p in "${protected_roots[@]}"; do
[[ -e "$p" ]] || continue
case "$path" in
"$p"|"$p"/*)
return 0
;;
esac
done
return 1
}
collect_empty_dirs() {
if [[ $ALL_EMPTY -eq 1 ]]; then
find "$TARGET_ROOT" -depth -type d -empty
else
for r in "${managed_roots[@]}"; do
[[ -d "$r" ]] || continue
find "$r" -depth -type d -empty
done
fi
}
count_candidates=0
count_deletable=0
while IFS= read -r d; do
[[ -n "$d" ]] || continue
count_candidates=$((count_candidates + 1))
# Never remove runtime root.
[[ "$d" == "$TARGET_ROOT" ]] && continue
if is_protected "$d"; then
continue
fi
count_deletable=$((count_deletable + 1))
if [[ $APPLY -eq 1 ]]; then
rmdir "$d" 2>/dev/null || true
if [[ ! -d "$d" ]]; then
echo "[mosaic-clean] deleted: $d"
fi
else
echo "[mosaic-clean] would delete: $d"
fi
done < <(collect_empty_dirs | sort -u)
mode="managed"
[[ $ALL_EMPTY -eq 1 ]] && mode="all-empty"
if [[ $APPLY -eq 1 ]]; then
echo "[mosaic-clean] complete: mode=$mode deleted_or_attempted=$count_deletable candidates=$count_candidates runtime=$RUNTIME"
else
echo "[mosaic-clean] dry-run: mode=$mode deletable=$count_deletable candidates=$count_candidates runtime=$RUNTIME"
echo "[mosaic-clean] re-run with --apply to delete"
fi

View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ -x "scripts/agent/critical.sh" ]]; then
exec bash scripts/agent/critical.sh
fi
echo "[mosaic] Missing scripts/agent/critical.sh in $(pwd)" >&2
exit 1

View File

@@ -0,0 +1,435 @@
#!/usr/bin/env bash
set -euo pipefail
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
FAIL_ON_WARN=0
VERBOSE=0
FIX_MODE=0
usage() {
cat <<USAGE
Usage: $(basename "$0") [options]
Audit Mosaic runtime state and detect drift across agent runtimes.
Options:
--fix Auto-fix: create missing dirs, wire skills into all harnesses
--fail-on-warn Exit non-zero when warnings are found
--verbose Print pass checks too
-h, --help Show help
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--fix)
FIX_MODE=1
shift
;;
--fail-on-warn)
FAIL_ON_WARN=1
shift
;;
--verbose)
VERBOSE=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
fix_count=0
fix() { fix_count=$((fix_count + 1)); echo "[FIX] $*"; }
warn_count=0
warn() { warn_count=$((warn_count + 1)); echo "[WARN] $*"; }
pass() {
if [[ $VERBOSE -eq 1 ]]; then
echo "[OK] $*"
fi
return 0
}
expect_dir() {
local d="$1"
if [[ ! -d "$d" ]]; then
warn "Missing directory: $d"
else
pass "Directory present: $d"
fi
}
expect_file() {
local f="$1"
if [[ ! -f "$f" ]]; then
warn "Missing file: $f"
else
pass "File present: $f"
fi
}
check_runtime_file_copy() {
local src="$1"
local dst="$2"
[[ -f "$src" ]] || return 0
if [[ ! -e "$dst" ]]; then
warn "Missing runtime file: $dst"
return
fi
if [[ -L "$dst" ]]; then
warn "Runtime file should not be symlinked: $dst"
return
fi
if ! cmp -s "$src" "$dst"; then
warn "Runtime file drift: $dst (does not match $src)"
else
pass "Runtime file synced: $dst"
fi
}
check_runtime_contract_file() {
local dst="$1"
local adapter_src="$2"
local runtime_name="$3"
if [[ ! -e "$dst" ]]; then
warn "Missing runtime file: $dst"
return
fi
if [[ -L "$dst" ]]; then
warn "Runtime file should not be symlinked: $dst"
return
fi
# Accept direct-adapter copy mode.
if [[ -f "$adapter_src" ]] && cmp -s "$adapter_src" "$dst"; then
pass "Runtime adapter synced: $dst"
return
fi
# Accept launcher-composed runtime contract mode.
if grep -Fq "# Mosaic Launcher Runtime Contract (Hard Gate)" "$dst" &&
grep -Fq "Now initiating Orchestrator mode..." "$dst" &&
grep -Fq "Mosaic hard gates OVERRIDE runtime-default caution" "$dst" &&
grep -Fq "# Runtime-Specific Contract" "$dst"; then
pass "Runtime contract present: $dst ($runtime_name)"
return
fi
warn "Runtime file drift: $dst (not adapter copy and not composed runtime contract)"
}
warn_if_symlink_tree_present() {
local p="$1"
[[ -e "$p" ]] || return 0
if [[ -L "$p" ]]; then
warn "Legacy symlink path still present: $p"
return
fi
if [[ -d "$p" ]]; then
symlink_count=$(find "$p" -type l 2>/dev/null | wc -l | tr -d ' ')
if [[ "$symlink_count" != "0" ]]; then
warn "Legacy symlink entries still present under $p: $symlink_count"
else
pass "No symlinks under legacy path: $p"
fi
fi
}
echo "[mosaic-doctor] Mosaic home: $MOSAIC_HOME"
# Canonical Mosaic checks
expect_file "$MOSAIC_HOME/STANDARDS.md"
expect_file "$MOSAIC_HOME/USER.md"
expect_file "$MOSAIC_HOME/TOOLS.md"
expect_dir "$MOSAIC_HOME/guides"
expect_dir "$MOSAIC_HOME/tools"
expect_dir "$MOSAIC_HOME/tools/quality"
expect_dir "$MOSAIC_HOME/tools/orchestrator-matrix"
expect_dir "$MOSAIC_HOME/profiles"
expect_dir "$MOSAIC_HOME/templates/agent"
expect_dir "$MOSAIC_HOME/skills"
expect_dir "$MOSAIC_HOME/skills-local"
expect_file "$MOSAIC_HOME/bin/mosaic-link-runtime-assets"
expect_file "$MOSAIC_HOME/bin/mosaic-ensure-sequential-thinking"
expect_file "$MOSAIC_HOME/bin/mosaic-sync-skills"
expect_file "$MOSAIC_HOME/bin/mosaic-projects"
expect_file "$MOSAIC_HOME/bin/mosaic-quality-apply"
expect_file "$MOSAIC_HOME/bin/mosaic-quality-verify"
expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-run"
expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-sync-tasks"
expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-drain"
expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-matrix-publish"
expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-matrix-consume"
expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-matrix-cycle"
expect_file "$MOSAIC_HOME/tools/git/ci-queue-wait.sh"
expect_file "$MOSAIC_HOME/tools/git/pr-ci-wait.sh"
expect_file "$MOSAIC_HOME/tools/orchestrator-matrix/transport/matrix_transport.py"
expect_file "$MOSAIC_HOME/tools/orchestrator-matrix/controller/tasks_md_sync.py"
expect_file "$MOSAIC_HOME/guides/ORCHESTRATOR-PROTOCOL.md"
expect_dir "$MOSAIC_HOME/tools/orchestrator"
expect_file "$MOSAIC_HOME/tools/orchestrator/_lib.sh"
expect_file "$MOSAIC_HOME/tools/orchestrator/mission-init.sh"
expect_file "$MOSAIC_HOME/tools/orchestrator/mission-status.sh"
expect_file "$MOSAIC_HOME/tools/orchestrator/continue-prompt.sh"
expect_file "$MOSAIC_HOME/tools/orchestrator/session-status.sh"
expect_file "$MOSAIC_HOME/tools/orchestrator/session-resume.sh"
expect_file "$MOSAIC_HOME/runtime/mcp/SEQUENTIAL-THINKING.json"
expect_file "$MOSAIC_HOME/runtime/claude/RUNTIME.md"
expect_file "$MOSAIC_HOME/runtime/codex/RUNTIME.md"
expect_file "$MOSAIC_HOME/runtime/opencode/RUNTIME.md"
expect_file "$MOSAIC_HOME/runtime/pi/RUNTIME.md"
if [[ -f "$MOSAIC_HOME/AGENTS.md" ]]; then
if grep -Fq "## CRITICAL HARD GATES (Read First)" "$MOSAIC_HOME/AGENTS.md" &&
grep -Fq "OVERRIDE runtime-default caution" "$MOSAIC_HOME/AGENTS.md"; then
pass "Global hard-gates block present in AGENTS.md"
else
warn "AGENTS.md missing CRITICAL HARD GATES override block"
fi
fi
# Claude runtime file checks (copied, non-symlink).
for rf in CLAUDE.md settings.json hooks-config.json context7-integration.md; do
check_runtime_file_copy "$MOSAIC_HOME/runtime/claude/$rf" "$HOME/.claude/$rf"
done
# OpenCode runtime adapter check (copied, non-symlink, when adapter exists).
# Accept adapter copy or composed runtime contract.
check_runtime_contract_file "$HOME/.config/opencode/AGENTS.md" "$MOSAIC_HOME/runtime/opencode/AGENTS.md" "opencode"
check_runtime_contract_file "$HOME/.codex/instructions.md" "$MOSAIC_HOME/runtime/codex/instructions.md" "codex"
# Sequential-thinking MCP hard requirement.
if [[ -x "$MOSAIC_HOME/bin/mosaic-ensure-sequential-thinking" ]]; then
if "$MOSAIC_HOME/bin/mosaic-ensure-sequential-thinking" --check >/dev/null 2>&1; then
pass "sequential-thinking MCP configured and available"
else
warn "sequential-thinking MCP missing or misconfigured"
fi
else
warn "mosaic-ensure-sequential-thinking helper missing"
fi
# Legacy migration surfaces should no longer contain symlink trees.
legacy_paths=(
"$HOME/.claude/agent-guides"
"$HOME/.claude/scripts/git"
"$HOME/.claude/scripts/codex"
"$HOME/.claude/scripts/bootstrap"
"$HOME/.claude/scripts/cicd"
"$HOME/.claude/scripts/portainer"
"$HOME/.claude/templates"
"$HOME/.claude/presets/domains"
"$HOME/.claude/presets/tech-stacks"
"$HOME/.claude/presets/workflows"
)
for p in "${legacy_paths[@]}"; do
warn_if_symlink_tree_present "$p"
done
# Skills runtime checks (still symlinked into runtime-specific skills dirs).
for runtime_skills in "$HOME/.claude/skills" "$HOME/.codex/skills" "$HOME/.config/opencode/skills" "$HOME/.pi/agent/skills"; do
[[ -d "$runtime_skills" ]] || continue
while IFS= read -r -d '' skill; do
name="$(basename "$skill")"
[[ "$name" == .* ]] && continue
target="$runtime_skills/$name"
if [[ ! -e "$target" ]]; then
warn "Missing skill link: $target"
continue
fi
if [[ ! -L "$target" ]]; then
warn "Non-symlink skill entry: $target"
continue
fi
target_real="$(readlink -f "$target" 2>/dev/null || true)"
skill_real="$(readlink -f "$skill" 2>/dev/null || true)"
if [[ -z "$target_real" || -z "$skill_real" || "$target_real" != "$skill_real" ]]; then
warn "Drifted skill link: $target (expected -> $skill)"
else
pass "Linked skill: $target"
fi
done < <(find "$MOSAIC_HOME/skills" "$MOSAIC_HOME/skills-local" -mindepth 1 -maxdepth 1 -type d -print0)
done
# Broken links only in managed runtime skill dirs.
link_roots=(
"$HOME/.claude/skills"
"$HOME/.codex/skills"
"$HOME/.config/opencode/skills"
"$HOME/.pi/agent/skills"
)
existing_link_roots=()
for d in "${link_roots[@]}"; do
[[ -e "$d" ]] && existing_link_roots+=("$d")
done
broken_links=0
if [[ ${#existing_link_roots[@]} -gt 0 ]]; then
broken_links=$(find "${existing_link_roots[@]}" -xtype l 2>/dev/null | wc -l | tr -d ' ')
fi
if [[ "$broken_links" != "0" ]]; then
warn "Broken skill symlinks detected: $broken_links"
fi
# Pi agent skills directory check.
if [[ ! -d "$HOME/.pi/agent/skills" ]]; then
warn "Pi skills directory missing: $HOME/.pi/agent/skills"
else
pass "Pi skills directory present: $HOME/.pi/agent/skills"
fi
# Pi settings.json — check skills path is configured.
pi_settings="$HOME/.pi/agent/settings.json"
if [[ -f "$pi_settings" ]]; then
if grep -q 'skills' "$pi_settings" 2>/dev/null; then
pass "Pi settings.json has skills configuration"
else
warn "Pi settings.json missing skills array — Mosaic skills may not load"
fi
fi
# Mosaic-specific skills presence check.
mosaic_skills=(mosaic-board mosaic-forge mosaic-prdy mosaic-macp mosaic-standards mosaic-prd mosaic-jarvis mosaic-setup-cicd)
for skill_name in "${mosaic_skills[@]}"; do
if [[ -d "$MOSAIC_HOME/skills/$skill_name" ]] || [[ -L "$MOSAIC_HOME/skills/$skill_name" ]]; then
pass "Mosaic skill present: $skill_name"
elif [[ -d "$MOSAIC_HOME/skills-local/$skill_name" ]]; then
pass "Mosaic skill present (local): $skill_name"
else
warn "Missing Mosaic skill: $skill_name"
fi
done
# ── --fix mode: auto-wire skills into all harness directories ──────────────
if [[ $FIX_MODE -eq 1 ]]; then
echo ""
echo "[mosaic-doctor] Running auto-fix..."
# 1. Ensure all harness skill directories exist
harness_skill_dirs=(
"$HOME/.claude/skills"
"$HOME/.codex/skills"
"$HOME/.config/opencode/skills"
"$HOME/.pi/agent/skills"
)
for hdir in "${harness_skill_dirs[@]}"; do
if [[ ! -d "$hdir" ]]; then
mkdir -p "$hdir"
fix "Created missing directory: $hdir"
fi
done
# 2. Wire all Mosaic skills (canonical + local) into every harness
skill_sources=("$MOSAIC_HOME/skills" "$MOSAIC_HOME/skills-local")
for hdir in "${harness_skill_dirs[@]}"; do
# Skip if target resolves to canonical dir (avoid self-link)
hdir_real="$(readlink -f "$hdir" 2>/dev/null || true)"
canonical_real="$(readlink -f "$MOSAIC_HOME/skills" 2>/dev/null || true)"
if [[ -n "$hdir_real" && -n "$canonical_real" && "$hdir_real" == "$canonical_real" ]]; then
continue
fi
for src_dir in "${skill_sources[@]}"; do
[[ -d "$src_dir" ]] || continue
while IFS= read -r -d '' skill_path; do
skill_name="$(basename "$skill_path")"
[[ "$skill_name" == .* ]] && continue
link_path="$hdir/$skill_name"
if [[ -L "$link_path" ]]; then
# Repoint if target differs
current_target="$(readlink -f "$link_path" 2>/dev/null || true)"
expected_target="$(readlink -f "$skill_path" 2>/dev/null || true)"
if [[ "$current_target" != "$expected_target" ]]; then
ln -sfn "$skill_path" "$link_path"
fix "Repointed skill link: $link_path -> $skill_path"
fi
elif [[ -e "$link_path" ]]; then
# Non-symlink entry — preserve runtime-specific override
continue
else
ln -s "$skill_path" "$link_path"
fix "Linked skill: $link_path -> $skill_path"
fi
done < <(find "$src_dir" -mindepth 1 -maxdepth 1 -type d -print0; find "$src_dir" -mindepth 1 -maxdepth 1 -type l -print0)
done
# Prune broken symlinks in this harness dir
while IFS= read -r -d '' broken_link; do
rm -f "$broken_link"
fix "Removed broken link: $broken_link"
done < <(find "$hdir" -mindepth 1 -maxdepth 1 -xtype l -print0 2>/dev/null)
done
# 3. Ensure Pi settings.json includes Mosaic skills path
pi_settings_dir="$HOME/.pi/agent"
pi_settings_file="$pi_settings_dir/settings.json"
mkdir -p "$pi_settings_dir"
if [[ ! -f "$pi_settings_file" ]]; then
echo '{}' > "$pi_settings_file"
fix "Created Pi settings.json: $pi_settings_file"
fi
# Add skills paths if not already present
mosaic_skills_path="$MOSAIC_HOME/skills"
mosaic_local_path="$MOSAIC_HOME/skills-local"
if ! grep -q "$mosaic_skills_path" "$pi_settings_file" 2>/dev/null; then
# Use a simple approach: read, patch, write
if command -v python3 >/dev/null 2>&1; then
python3 -c "
import json, sys
with open('$pi_settings_file', 'r') as f:
data = json.load(f)
skills = data.get('skills', [])
if not isinstance(skills, list):
skills = []
for p in ['$mosaic_skills_path', '$mosaic_local_path']:
if p not in skills:
skills.append(p)
data['skills'] = skills
with open('$pi_settings_file', 'w') as f:
json.dump(data, f, indent=2)
f.write('\\n')
" 2>/dev/null && fix "Added Mosaic skills paths to Pi settings.json"
else
warn "python3 not available — cannot patch Pi settings.json. Add manually: skills: [\"$mosaic_skills_path\", \"$mosaic_local_path\"]"
fi
fi
# 4. Run link-runtime-assets if available
if [[ -x "$MOSAIC_HOME/bin/mosaic-link-runtime-assets" ]]; then
"$MOSAIC_HOME/bin/mosaic-link-runtime-assets" >/dev/null 2>&1 && fix "Re-ran mosaic-link-runtime-assets"
fi
echo "[mosaic-doctor] fixes=$fix_count"
fi
echo "[mosaic-doctor] warnings=$warn_count"
if [[ $FAIL_ON_WARN -eq 1 && $warn_count -gt 0 ]]; then
exit 1
fi

View File

@@ -0,0 +1,119 @@
#!/usr/bin/env bash
set -euo pipefail
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
TOOLS_DIR="$MOSAIC_HOME/tools/excalidraw"
MODE="apply"
SCOPE="user"
err() { echo "[mosaic-excalidraw] ERROR: $*" >&2; }
log() { echo "[mosaic-excalidraw] $*"; }
while [[ $# -gt 0 ]]; do
case "$1" in
--check) MODE="check"; shift ;;
--scope)
if [[ $# -lt 2 ]]; then
err "--scope requires a value: user|local"
exit 2
fi
SCOPE="$2"
shift 2
;;
*)
err "Unknown argument: $1"
exit 2
;;
esac
done
require_binary() {
local name="$1"
if ! command -v "$name" >/dev/null 2>&1; then
err "Required binary missing: $name"
return 1
fi
}
check_software() {
require_binary node
require_binary npm
}
check_tool_dir() {
[[ -d "$TOOLS_DIR" ]] || { err "Tool dir not found: $TOOLS_DIR"; return 1; }
[[ -f "$TOOLS_DIR/package.json" ]] || { err "package.json not found in $TOOLS_DIR"; return 1; }
[[ -f "$TOOLS_DIR/launch.sh" ]] || { err "launch.sh not found in $TOOLS_DIR"; return 1; }
}
check_npm_deps() {
[[ -d "$TOOLS_DIR/node_modules/@modelcontextprotocol" ]] || return 1
[[ -d "$TOOLS_DIR/node_modules/@excalidraw" ]] || return 1
[[ -d "$TOOLS_DIR/node_modules/jsdom" ]] || return 1
}
install_npm_deps() {
if check_npm_deps; then
return 0
fi
log "Installing npm deps in $TOOLS_DIR..."
(cd "$TOOLS_DIR" && npm install --silent) || {
err "npm install failed in $TOOLS_DIR"
return 1
}
}
check_claude_config() {
python3 - <<'PY'
import json
from pathlib import Path
p = Path.home() / ".claude.json"
if not p.exists():
raise SystemExit(1)
try:
data = json.loads(p.read_text(encoding="utf-8"))
except Exception:
raise SystemExit(1)
mcp = data.get("mcpServers")
if not isinstance(mcp, dict):
raise SystemExit(1)
entry = mcp.get("excalidraw")
if not isinstance(entry, dict):
raise SystemExit(1)
cmd = entry.get("command", "")
if not cmd.endswith("launch.sh"):
raise SystemExit(1)
PY
}
apply_claude_config() {
require_binary claude
local launch_sh="$TOOLS_DIR/launch.sh"
claude mcp add --scope user excalidraw -- "$launch_sh"
}
# ── Check mode ────────────────────────────────────────────────────────────────
if [[ "$MODE" == "check" ]]; then
check_software
check_tool_dir
if ! check_npm_deps; then
err "npm deps not installed in $TOOLS_DIR (run without --check to install)"
exit 1
fi
if ! check_claude_config; then
err "excalidraw not registered in ~/.claude.json"
exit 1
fi
log "excalidraw MCP is configured and available"
exit 0
fi
# ── Apply mode ────────────────────────────────────────────────────────────────
check_software
check_tool_dir
install_npm_deps
apply_claude_config
log "excalidraw MCP configured (scope: $SCOPE)"

View File

@@ -0,0 +1,262 @@
#!/usr/bin/env bash
set -euo pipefail
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
MODE="apply"
RUNTIME="all"
STRICT_CHECK=0
PKG="@modelcontextprotocol/server-sequential-thinking"
err() { echo "[mosaic-seq] ERROR: $*" >&2; }
log() { echo "[mosaic-seq] $*"; }
while [[ $# -gt 0 ]]; do
case "$1" in
--check)
MODE="check"
shift
;;
--runtime)
if [[ $# -lt 2 ]]; then
err "--runtime requires a value: claude|codex|opencode|all"
exit 2
fi
RUNTIME="$2"
shift 2
;;
--strict)
STRICT_CHECK=1
shift
;;
*)
err "Unknown argument: $1"
exit 2
;;
esac
done
case "$RUNTIME" in
all|claude|codex|opencode) ;;
*)
err "Invalid runtime: $RUNTIME (expected claude|codex|opencode|all)"
exit 2
;;
esac
require_binary() {
local name="$1"
if ! command -v "$name" >/dev/null 2>&1; then
err "Required binary missing: $name"
return 1
fi
}
check_software() {
require_binary node
require_binary npx
}
warm_package() {
local timeout_sec="${MOSAIC_SEQ_WARM_TIMEOUT_SEC:-15}"
if command -v timeout >/dev/null 2>&1; then
timeout "$timeout_sec" npx -y "$PKG" --help >/dev/null 2>&1
else
npx -y "$PKG" --help >/dev/null 2>&1
fi
}
check_claude_config() {
python3 - <<'PY'
import json
from pathlib import Path
p = Path.home() / ".claude" / "settings.json"
if not p.exists():
raise SystemExit(1)
try:
data = json.loads(p.read_text(encoding="utf-8"))
except Exception:
raise SystemExit(1)
mcp = data.get("mcpServers")
if not isinstance(mcp, dict):
raise SystemExit(1)
entry = mcp.get("sequential-thinking")
if not isinstance(entry, dict):
raise SystemExit(1)
if entry.get("command") != "npx":
raise SystemExit(1)
args = entry.get("args")
if args != ["-y", "@modelcontextprotocol/server-sequential-thinking"]:
raise SystemExit(1)
PY
}
apply_claude_config() {
python3 - <<'PY'
import json
from pathlib import Path
p = Path.home() / ".claude" / "settings.json"
p.parent.mkdir(parents=True, exist_ok=True)
if p.exists():
try:
data = json.loads(p.read_text(encoding="utf-8"))
except Exception:
data = {}
else:
data = {}
mcp = data.get("mcpServers")
if not isinstance(mcp, dict):
mcp = {}
mcp["sequential-thinking"] = {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-sequential-thinking"]
}
data["mcpServers"] = mcp
p.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
PY
}
check_codex_config() {
local cfg="$HOME/.codex/config.toml"
[[ -f "$cfg" ]] || return 1
grep -Eq '^\[mcp_servers\.(sequential-thinking|sequential_thinking)\]' "$cfg" && \
grep -q '^command = "npx"' "$cfg" && \
grep -q '@modelcontextprotocol/server-sequential-thinking' "$cfg"
}
apply_codex_config() {
local cfg="$HOME/.codex/config.toml"
mkdir -p "$(dirname "$cfg")"
[[ -f "$cfg" ]] || touch "$cfg"
local tmp
tmp="$(mktemp)"
awk '
BEGIN { skip = 0 }
/^\[mcp_servers\.(sequential-thinking|sequential_thinking)\]/ { skip = 1; next }
skip && /^\[/ { skip = 0 }
!skip { print }
' "$cfg" > "$tmp"
mv "$tmp" "$cfg"
{
echo ""
echo "[mcp_servers.sequential-thinking]"
echo "command = \"npx\""
echo "args = [\"-y\", \"@modelcontextprotocol/server-sequential-thinking\"]"
} >> "$cfg"
}
check_opencode_config() {
python3 - <<'PY'
import json
from pathlib import Path
p = Path.home() / ".config" / "opencode" / "config.json"
if not p.exists():
raise SystemExit(1)
try:
data = json.loads(p.read_text(encoding="utf-8"))
except Exception:
raise SystemExit(1)
mcp = data.get("mcp")
if not isinstance(mcp, dict):
raise SystemExit(1)
entry = mcp.get("sequential-thinking")
if not isinstance(entry, dict):
raise SystemExit(1)
if entry.get("type") != "local":
raise SystemExit(1)
if entry.get("command") != ["npx", "-y", "@modelcontextprotocol/server-sequential-thinking"]:
raise SystemExit(1)
if entry.get("enabled") is not True:
raise SystemExit(1)
PY
}
apply_opencode_config() {
python3 - <<'PY'
import json
from pathlib import Path
p = Path.home() / ".config" / "opencode" / "config.json"
p.parent.mkdir(parents=True, exist_ok=True)
if p.exists():
try:
data = json.loads(p.read_text(encoding="utf-8"))
except Exception:
data = {}
else:
data = {}
mcp = data.get("mcp")
if not isinstance(mcp, dict):
mcp = {}
mcp["sequential-thinking"] = {
"type": "local",
"command": ["npx", "-y", "@modelcontextprotocol/server-sequential-thinking"],
"enabled": True
}
data["mcp"] = mcp
p.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
PY
}
check_runtime_config() {
case "$RUNTIME" in
all)
check_claude_config
check_codex_config
check_opencode_config
;;
claude)
check_claude_config
;;
codex)
check_codex_config
;;
opencode)
check_opencode_config
;;
esac
}
apply_runtime_config() {
case "$RUNTIME" in
all)
apply_claude_config
apply_codex_config
apply_opencode_config
;;
claude)
apply_claude_config
;;
codex)
apply_codex_config
;;
opencode)
apply_opencode_config
;;
esac
}
if [[ "$MODE" == "check" ]]; then
check_software
check_runtime_config
# Runtime launch checks should be local/fast by default.
if [[ "$STRICT_CHECK" -eq 1 || "${MOSAIC_SEQ_CHECK_WARM:-0}" == "1" ]]; then
if ! warm_package; then
err "sequential-thinking package warm-up failed in strict mode"
exit 1
fi
fi
log "sequential-thinking MCP is configured and available (${RUNTIME})"
exit 0
fi
check_software
if ! warm_package; then
err "Unable to warm sequential-thinking package (npx timeout/failure)"
exit 1
fi
apply_runtime_config
log "sequential-thinking MCP configured (${RUNTIME})"

View File

@@ -0,0 +1,424 @@
#!/usr/bin/env bash
set -euo pipefail
# mosaic-init — Interactive agent identity, user profile, and tool config generator
#
# Usage:
# mosaic-init # Interactive mode
# mosaic-init --name "Jarvis" --style direct # Flag overrides
# mosaic-init --name "Jarvis" --role "memory steward" --style direct \
# --accessibility "ADHD-friendly chunking" --guardrails "Never auto-commit"
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
SOUL_TEMPLATE="$MOSAIC_HOME/templates/SOUL.md.template"
USER_TEMPLATE="$MOSAIC_HOME/templates/USER.md.template"
TOOLS_TEMPLATE="$MOSAIC_HOME/templates/TOOLS.md.template"
SOUL_OUTPUT="$MOSAIC_HOME/SOUL.md"
USER_OUTPUT="$MOSAIC_HOME/USER.md"
TOOLS_OUTPUT="$MOSAIC_HOME/TOOLS.md"
# Defaults
AGENT_NAME=""
ROLE_DESCRIPTION=""
STYLE=""
ACCESSIBILITY=""
CUSTOM_GUARDRAILS=""
# USER.md defaults
USER_NAME=""
PRONOUNS=""
TIMEZONE=""
BACKGROUND=""
COMMUNICATION_PREFS=""
PERSONAL_BOUNDARIES=""
PROJECTS_TABLE=""
# TOOLS.md defaults
GIT_PROVIDERS_TABLE=""
CREDENTIALS_LOCATION=""
CUSTOM_TOOLS_SECTION=""
usage() {
cat <<USAGE
Usage: $(basename "$0") [options]
Generate Mosaic identity and configuration files:
- SOUL.md — Agent identity contract
- USER.md — User profile and accessibility
- TOOLS.md — Machine-level tool reference
Interactive by default. Use flags to skip prompts.
Options:
--name <name> Agent name (e.g., "Jarvis", "Assistant")
--role <description> Role description (e.g., "memory steward, execution partner")
--style <style> Communication style: direct, friendly, or formal
--accessibility <prefs> Accessibility preferences (e.g., "ADHD-friendly chunking")
--guardrails <rules> Custom guardrails (appended to defaults)
--user-name <name> Your name for USER.md
--pronouns <pronouns> Your pronouns (e.g., "He/Him")
--timezone <tz> Your timezone (e.g., "America/Chicago")
--non-interactive Fail if any required value is missing (no prompts)
--soul-only Only generate SOUL.md
-h, --help Show help
USAGE
}
NON_INTERACTIVE=0
SOUL_ONLY=0
while [[ $# -gt 0 ]]; do
case "$1" in
--name) AGENT_NAME="$2"; shift 2 ;;
--role) ROLE_DESCRIPTION="$2"; shift 2 ;;
--style) STYLE="$2"; shift 2 ;;
--accessibility) ACCESSIBILITY="$2"; shift 2 ;;
--guardrails) CUSTOM_GUARDRAILS="$2"; shift 2 ;;
--user-name) USER_NAME="$2"; shift 2 ;;
--pronouns) PRONOUNS="$2"; shift 2 ;;
--timezone) TIMEZONE="$2"; shift 2 ;;
--non-interactive) NON_INTERACTIVE=1; shift ;;
--soul-only) SOUL_ONLY=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;;
esac
done
prompt_if_empty() {
local var_name="$1"
local prompt_text="$2"
local default_value="${3:-}"
local current_value="${!var_name}"
if [[ -n "$current_value" ]]; then
return
fi
if [[ $NON_INTERACTIVE -eq 1 ]]; then
if [[ -n "$default_value" ]]; then
eval "$var_name=\"$default_value\""
return
fi
echo "[mosaic-init] ERROR: --$var_name is required in non-interactive mode" >&2
exit 1
fi
if [[ -n "$default_value" ]]; then
prompt_text="$prompt_text [$default_value]"
fi
printf "%s: " "$prompt_text"
read -r value
if [[ -z "$value" && -n "$default_value" ]]; then
value="$default_value"
fi
eval "$var_name=\"$value\""
}
prompt_multiline() {
local var_name="$1"
local prompt_text="$2"
local default_value="${3:-}"
local current_value="${!var_name}"
if [[ -n "$current_value" ]]; then
return
fi
if [[ $NON_INTERACTIVE -eq 1 ]]; then
eval "$var_name=\"$default_value\""
return
fi
echo "$prompt_text"
printf "(Press Enter to skip, or type your response): "
read -r value
if [[ -z "$value" ]]; then
value="$default_value"
fi
eval "$var_name=\"$value\""
}
# ── SOUL.md Generation ────────────────────────────────────────
echo "[mosaic-init] Generating SOUL.md — agent identity contract"
echo ""
prompt_if_empty AGENT_NAME "What name should agents use" "Assistant"
prompt_if_empty ROLE_DESCRIPTION "Agent role description" "execution partner and visibility engine"
if [[ -z "$STYLE" && $NON_INTERACTIVE -eq 0 ]]; then
echo ""
echo "Communication style:"
echo " 1) direct — Concise, no fluff, actionable"
echo " 2) friendly — Warm but efficient, conversational"
echo " 3) formal — Professional, structured, thorough"
printf "Choose [1/2/3] (default: 1): "
read -r style_choice
case "${style_choice:-1}" in
1|direct) STYLE="direct" ;;
2|friendly) STYLE="friendly" ;;
3|formal) STYLE="formal" ;;
*) STYLE="direct" ;;
esac
elif [[ -z "$STYLE" ]]; then
STYLE="direct"
fi
prompt_if_empty ACCESSIBILITY "Accessibility preferences (or 'none')" "none"
if [[ $NON_INTERACTIVE -eq 0 && -z "$CUSTOM_GUARDRAILS" ]]; then
echo ""
printf "Custom guardrails (optional, press Enter to skip): "
read -r CUSTOM_GUARDRAILS
fi
# Build behavioral principles based on style + accessibility
BEHAVIORAL_PRINCIPLES=""
case "$STYLE" in
direct)
BEHAVIORAL_PRINCIPLES="1. Clarity over performance theater.
2. Practical execution over abstract planning.
3. Truthfulness over confidence: state uncertainty explicitly.
4. Visible state over hidden assumptions.
5. Accessibility-aware — see \`~/.config/mosaic/USER.md\` for user-specific accommodations."
;;
friendly)
BEHAVIORAL_PRINCIPLES="1. Be helpful and approachable while staying efficient.
2. Provide context and explain reasoning when helpful.
3. Truthfulness over confidence: state uncertainty explicitly.
4. Visible state over hidden assumptions.
5. Accessibility-aware — see \`~/.config/mosaic/USER.md\` for user-specific accommodations."
;;
formal)
BEHAVIORAL_PRINCIPLES="1. Maintain professional, structured communication.
2. Provide thorough analysis with explicit tradeoffs.
3. Truthfulness over confidence: state uncertainty explicitly.
4. Document decisions and rationale clearly.
5. Accessibility-aware — see \`~/.config/mosaic/USER.md\` for user-specific accommodations."
;;
esac
if [[ "$ACCESSIBILITY" != "none" && -n "$ACCESSIBILITY" ]]; then
BEHAVIORAL_PRINCIPLES="$BEHAVIORAL_PRINCIPLES
6. $ACCESSIBILITY."
fi
# Build communication style section
COMMUNICATION_STYLE=""
case "$STYLE" in
direct)
COMMUNICATION_STYLE="- Be direct, concise, and concrete.
- Avoid fluff, hype, and anthropomorphic roleplay.
- Do not simulate certainty when facts are missing.
- Prefer actionable next steps and explicit tradeoffs."
;;
friendly)
COMMUNICATION_STYLE="- Be warm and conversational while staying focused.
- Explain your reasoning when it helps the user.
- Do not simulate certainty when facts are missing.
- Prefer actionable next steps with clear context."
;;
formal)
COMMUNICATION_STYLE="- Use professional, structured language.
- Provide thorough explanations with supporting detail.
- Do not simulate certainty when facts are missing.
- Present options with explicit tradeoffs and recommendations."
;;
esac
# Format custom guardrails
FORMATTED_GUARDRAILS=""
if [[ -n "$CUSTOM_GUARDRAILS" ]]; then
FORMATTED_GUARDRAILS="- $CUSTOM_GUARDRAILS"
fi
# Verify template exists
if [[ ! -f "$SOUL_TEMPLATE" ]]; then
echo "[mosaic-init] ERROR: Template not found: $SOUL_TEMPLATE" >&2
echo "[mosaic-init] Run the Mosaic installer first." >&2
exit 1
fi
# Generate SOUL.md from template using awk (handles multi-line values)
awk -v name="$AGENT_NAME" \
-v role="$ROLE_DESCRIPTION" \
-v principles="$BEHAVIORAL_PRINCIPLES" \
-v comms="$COMMUNICATION_STYLE" \
-v guardrails="$FORMATTED_GUARDRAILS" \
'{
gsub(/\{\{AGENT_NAME\}\}/, name)
gsub(/\{\{ROLE_DESCRIPTION\}\}/, role)
gsub(/\{\{BEHAVIORAL_PRINCIPLES\}\}/, principles)
gsub(/\{\{COMMUNICATION_STYLE\}\}/, comms)
gsub(/\{\{CUSTOM_GUARDRAILS\}\}/, guardrails)
print
}' "$SOUL_TEMPLATE" > "$SOUL_OUTPUT"
echo ""
echo "[mosaic-init] Generated: $SOUL_OUTPUT"
echo "[mosaic-init] Agent name: $AGENT_NAME"
echo "[mosaic-init] Style: $STYLE"
if [[ $SOUL_ONLY -eq 1 ]]; then
# Push to runtime adapters and exit
if [[ -x "$MOSAIC_HOME/bin/mosaic-link-runtime-assets" ]]; then
echo "[mosaic-init] Updating runtime adapters..."
"$MOSAIC_HOME/bin/mosaic-link-runtime-assets"
fi
echo "[mosaic-init] Done. Launch with: mosaic claude"
exit 0
fi
# ── USER.md Generation ────────────────────────────────────────
echo ""
echo "[mosaic-init] Generating USER.md — user profile"
echo ""
prompt_if_empty USER_NAME "Your name" ""
prompt_if_empty PRONOUNS "Your pronouns" "They/Them"
prompt_if_empty TIMEZONE "Your timezone" "UTC"
prompt_multiline BACKGROUND "Your professional background (brief summary)" "(not configured)"
# Build accessibility section
ACCESSIBILITY_SECTION=""
if [[ "$ACCESSIBILITY" != "none" && -n "$ACCESSIBILITY" ]]; then
ACCESSIBILITY_SECTION="$ACCESSIBILITY"
else
if [[ $NON_INTERACTIVE -eq 0 ]]; then
echo ""
prompt_multiline ACCESSIBILITY_SECTION \
"Accessibility or neurodivergence accommodations (or press Enter to skip)" \
"(No specific accommodations configured. Edit this section to add any.)"
else
ACCESSIBILITY_SECTION="(No specific accommodations configured. Edit this section to add any.)"
fi
fi
# Build communication preferences
if [[ -z "$COMMUNICATION_PREFS" ]]; then
case "$STYLE" in
direct)
COMMUNICATION_PREFS="- Direct and concise
- No sycophancy
- Executive summaries and tables for overview"
;;
friendly)
COMMUNICATION_PREFS="- Warm and conversational
- Explain reasoning when helpful
- Balance thoroughness with brevity"
;;
formal)
COMMUNICATION_PREFS="- Professional and structured
- Thorough explanations with supporting detail
- Formal tone with explicit recommendations"
;;
esac
fi
prompt_multiline PERSONAL_BOUNDARIES \
"Personal boundaries or preferences agents should respect" \
"(Edit this section to add any personal boundaries.)"
if [[ -z "$PROJECTS_TABLE" ]]; then
PROJECTS_TABLE="| Project | Stack | Registry |
|---------|-------|----------|
| (none configured) | | |"
fi
if [[ ! -f "$USER_TEMPLATE" ]]; then
echo "[mosaic-init] WARN: USER.md template not found: $USER_TEMPLATE" >&2
echo "[mosaic-init] Skipping USER.md generation." >&2
else
awk -v user_name="$USER_NAME" \
-v pronouns="$PRONOUNS" \
-v timezone="$TIMEZONE" \
-v background="$BACKGROUND" \
-v accessibility="$ACCESSIBILITY_SECTION" \
-v comms="$COMMUNICATION_PREFS" \
-v boundaries="$PERSONAL_BOUNDARIES" \
-v projects="$PROJECTS_TABLE" \
'{
gsub(/\{\{USER_NAME\}\}/, user_name)
gsub(/\{\{PRONOUNS\}\}/, pronouns)
gsub(/\{\{TIMEZONE\}\}/, timezone)
gsub(/\{\{BACKGROUND\}\}/, background)
gsub(/\{\{ACCESSIBILITY_SECTION\}\}/, accessibility)
gsub(/\{\{COMMUNICATION_PREFS\}\}/, comms)
gsub(/\{\{PERSONAL_BOUNDARIES\}\}/, boundaries)
gsub(/\{\{PROJECTS_TABLE\}\}/, projects)
print
}' "$USER_TEMPLATE" > "$USER_OUTPUT"
echo "[mosaic-init] Generated: $USER_OUTPUT"
fi
# ── TOOLS.md Generation ───────────────────────────────────────
echo ""
echo "[mosaic-init] Generating TOOLS.md — machine-level tool reference"
echo ""
if [[ -z "$GIT_PROVIDERS_TABLE" ]]; then
if [[ $NON_INTERACTIVE -eq 0 ]]; then
echo "Git providers (add rows for your Gitea/GitHub/GitLab instances):"
printf "Primary git provider URL (or press Enter to skip): "
read -r git_url
if [[ -n "$git_url" ]]; then
printf "Provider name: "
read -r git_name
printf "CLI tool (tea/gh/glab): "
read -r git_cli
printf "Purpose: "
read -r git_purpose
GIT_PROVIDERS_TABLE="| Instance | URL | CLI | Purpose |
|----------|-----|-----|---------|
| $git_name | $git_url | \`$git_cli\` | $git_purpose |"
else
GIT_PROVIDERS_TABLE="| Instance | URL | CLI | Purpose |
|----------|-----|-----|---------|
| (add your git providers here) | | | |"
fi
else
GIT_PROVIDERS_TABLE="| Instance | URL | CLI | Purpose |
|----------|-----|-----|---------|
| (add your git providers here) | | | |"
fi
fi
prompt_if_empty CREDENTIALS_LOCATION "Credential file path (or 'none')" "none"
if [[ -z "$CUSTOM_TOOLS_SECTION" ]]; then
CUSTOM_TOOLS_SECTION="## Custom Tools
(Add any machine-specific tools, scripts, or workflows here.)"
fi
if [[ ! -f "$TOOLS_TEMPLATE" ]]; then
echo "[mosaic-init] WARN: TOOLS.md template not found: $TOOLS_TEMPLATE" >&2
echo "[mosaic-init] Skipping TOOLS.md generation." >&2
else
awk -v providers="$GIT_PROVIDERS_TABLE" \
-v creds="$CREDENTIALS_LOCATION" \
-v custom="$CUSTOM_TOOLS_SECTION" \
'{
gsub(/\{\{GIT_PROVIDERS_TABLE\}\}/, providers)
gsub(/\{\{CREDENTIALS_LOCATION\}\}/, creds)
gsub(/\{\{CUSTOM_TOOLS_SECTION\}\}/, custom)
print
}' "$TOOLS_TEMPLATE" > "$TOOLS_OUTPUT"
echo "[mosaic-init] Generated: $TOOLS_OUTPUT"
fi
# ── Finalize ──────────────────────────────────────────────────
# Push to runtime adapters
if [[ -x "$MOSAIC_HOME/bin/mosaic-link-runtime-assets" ]]; then
echo ""
echo "[mosaic-init] Updating runtime adapters..."
"$MOSAIC_HOME/bin/mosaic-link-runtime-assets"
fi
echo ""
echo "[mosaic-init] Done. Launch with: mosaic claude"
echo "[mosaic-init] Edit USER.md and TOOLS.md directly for further customization."

View File

@@ -0,0 +1,136 @@
#!/usr/bin/env bash
set -euo pipefail
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
backup_stamp="$(date +%Y%m%d%H%M%S)"
copy_file_managed() {
local src="$1"
local dst="$2"
mkdir -p "$(dirname "$dst")"
if [[ -L "$dst" ]]; then
rm -f "$dst"
fi
if [[ -f "$dst" ]]; then
if cmp -s "$src" "$dst"; then
return
fi
mv "$dst" "${dst}.mosaic-bak-${backup_stamp}"
fi
cp "$src" "$dst"
}
remove_legacy_path() {
local p="$1"
if [[ -L "$p" ]]; then
rm -f "$p"
return
fi
if [[ -d "$p" ]]; then
find "$p" -depth -type l -delete 2>/dev/null || true
find "$p" -depth -type d -empty -delete 2>/dev/null || true
return
fi
# Remove stale symlinked files if present.
if [[ -e "$p" && -L "$p" ]]; then
rm -f "$p"
fi
}
# Remove compatibility symlink surfaces for migrated content.
legacy_paths=(
"$HOME/.claude/agent-guides"
"$HOME/.claude/scripts/git"
"$HOME/.claude/scripts/codex"
"$HOME/.claude/scripts/bootstrap"
"$HOME/.claude/scripts/cicd"
"$HOME/.claude/scripts/portainer"
"$HOME/.claude/scripts/debug-hook.sh"
"$HOME/.claude/scripts/qa-hook-handler.sh"
"$HOME/.claude/scripts/qa-hook-stdin.sh"
"$HOME/.claude/scripts/qa-hook-wrapper.sh"
"$HOME/.claude/scripts/qa-queue-monitor.sh"
"$HOME/.claude/scripts/remediation-hook-handler.sh"
"$HOME/.claude/templates"
"$HOME/.claude/presets/domains"
"$HOME/.claude/presets/tech-stacks"
"$HOME/.claude/presets/workflows"
"$HOME/.claude/presets/jarvis-loop.json"
)
for p in "${legacy_paths[@]}"; do
remove_legacy_path "$p"
done
# Claude-specific runtime files (settings, hooks — NOT CLAUDE.md which is now a thin pointer)
for runtime_file in \
CLAUDE.md \
settings.json \
hooks-config.json \
context7-integration.md; do
src="$MOSAIC_HOME/runtime/claude/$runtime_file"
[[ -f "$src" ]] || continue
copy_file_managed "$src" "$HOME/.claude/$runtime_file"
done
# OpenCode runtime adapter (thin pointer to AGENTS.md)
opencode_adapter="$MOSAIC_HOME/runtime/opencode/AGENTS.md"
if [[ -f "$opencode_adapter" ]]; then
copy_file_managed "$opencode_adapter" "$HOME/.config/opencode/AGENTS.md"
fi
# Codex runtime adapter (thin pointer to AGENTS.md)
codex_adapter="$MOSAIC_HOME/runtime/codex/instructions.md"
if [[ -f "$codex_adapter" ]]; then
mkdir -p "$HOME/.codex"
copy_file_managed "$codex_adapter" "$HOME/.codex/instructions.md"
fi
# Pi runtime settings (MCP + skills paths)
pi_settings_dir="$HOME/.pi/agent"
pi_settings_file="$pi_settings_dir/settings.json"
mkdir -p "$pi_settings_dir"
if [[ ! -f "$pi_settings_file" ]]; then
echo '{}' > "$pi_settings_file"
fi
# Ensure Pi settings.json has Mosaic skills paths
mosaic_skills_path="$MOSAIC_HOME/skills"
mosaic_local_path="$MOSAIC_HOME/skills-local"
if ! grep -q "$mosaic_skills_path" "$pi_settings_file" 2>/dev/null; then
if command -v python3 >/dev/null 2>&1; then
python3 -c "
import json
with open('$pi_settings_file', 'r') as f:
data = json.load(f)
skills = data.get('skills', [])
if not isinstance(skills, list):
skills = []
for p in ['$mosaic_skills_path', '$mosaic_local_path']:
if p not in skills:
skills.append(p)
data['skills'] = skills
with open('$pi_settings_file', 'w') as f:
json.dump(data, f, indent=2)
f.write('\\n')
" 2>/dev/null
fi
fi
# Pi extension is loaded via --extension flag in the mosaic launcher.
# Do NOT copy into ~/.pi/agent/extensions/ — that causes duplicate loading.
if [[ -x "$MOSAIC_HOME/bin/mosaic-ensure-sequential-thinking" ]]; then
"$MOSAIC_HOME/bin/mosaic-ensure-sequential-thinking"
fi
echo "[mosaic-link] Runtime assets synced (non-symlink mode)"
echo "[mosaic-link] Canonical source: $MOSAIC_HOME"

View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ -x "scripts/agent/log-limitation.sh" ]]; then
exec bash scripts/agent/log-limitation.sh "$@"
fi
echo "[mosaic] Missing scripts/agent/log-limitation.sh in $(pwd)" >&2
exit 1

View File

@@ -0,0 +1,88 @@
#!/usr/bin/env bash
set -euo pipefail
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
APPLY=0
usage() {
cat <<USAGE
Usage: $(basename "$0") [--apply]
Migrate runtime-local skill directories (e.g. ~/.claude/skills/jarvis) to Mosaic-managed
skills by replacing local directories with symlinks to ~/.config/mosaic/skills-local.
Default mode is dry-run.
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--apply)
APPLY=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
skill_roots=(
"$HOME/.claude/skills"
"$HOME/.codex/skills"
"$HOME/.config/opencode/skills"
"$HOME/.pi/agent/skills"
)
if [[ ! -d "$MOSAIC_HOME/skills-local" ]]; then
echo "[mosaic-local-skills] Missing local skills dir: $MOSAIC_HOME/skills-local" >&2
exit 1
fi
count=0
while IFS= read -r -d '' local_skill; do
name="$(basename "$local_skill")"
src="$MOSAIC_HOME/skills-local/$name"
[[ -d "$src" ]] || continue
for root in "${skill_roots[@]}"; do
[[ -d "$root" ]] || continue
target="$root/$name"
# Already linked correctly.
if [[ -L "$target" ]]; then
target_real="$(readlink -f "$target" 2>/dev/null || true)"
src_real="$(readlink -f "$src" 2>/dev/null || true)"
if [[ -n "$target_real" && -n "$src_real" && "$target_real" == "$src_real" ]]; then
continue
fi
fi
# Only migrate local directories containing SKILL.md
if [[ -d "$target" && -f "$target/SKILL.md" && ! -L "$target" ]]; then
count=$((count + 1))
if [[ $APPLY -eq 1 ]]; then
stamp="$(date +%Y%m%d%H%M%S)"
mv "$target" "${target}.mosaic-bak-${stamp}"
ln -s "$src" "$target"
echo "[mosaic-local-skills] migrated: $target -> $src"
else
echo "[mosaic-local-skills] would migrate: $target -> $src"
fi
fi
done
done < <(find "$MOSAIC_HOME/skills-local" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) -print0)
if [[ $APPLY -eq 1 ]]; then
echo "[mosaic-local-skills] complete: migrated=$count"
else
echo "[mosaic-local-skills] dry-run: migratable=$count"
echo "[mosaic-local-skills] re-run with --apply to migrate"
fi

View File

@@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -euo pipefail
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
sync_cmd="$MOSAIC_HOME/bin/mosaic-orchestrator-sync-tasks"
run_cmd="$MOSAIC_HOME/bin/mosaic-orchestrator-run"
do_sync=1
poll_sec=15
extra_args=()
while [[ $# -gt 0 ]]; do
case "$1" in
--no-sync)
do_sync=0
shift
;;
--poll-sec)
poll_sec="${2:-15}"
shift 2
;;
*)
extra_args+=("$1")
shift
;;
esac
done
if [[ $do_sync -eq 1 ]]; then
"$sync_cmd" --apply
fi
exec "$run_cmd" --until-drained --poll-sec "$poll_sec" "${extra_args[@]}"

View File

@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
BRIDGE="$MOSAIC_HOME/tools/orchestrator-matrix/transport/matrix_transport.py"
if [[ ! -f "$BRIDGE" ]]; then
echo "[mosaic-orch-matrix] missing transport bridge: $BRIDGE" >&2
exit 1
fi
exec python3 "$BRIDGE" --repo "$(pwd)" --mode consume "$@"

View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
consume="$MOSAIC_HOME/bin/mosaic-orchestrator-matrix-consume"
run="$MOSAIC_HOME/bin/mosaic-orchestrator-run"
publish="$MOSAIC_HOME/bin/mosaic-orchestrator-matrix-publish"
for cmd in "$consume" "$run" "$publish"; do
if [[ ! -x "$cmd" ]]; then
echo "[mosaic-orch-cycle] missing executable: $cmd" >&2
exit 1
fi
done
"$consume"
"$run" --once "$@"
"$publish"

View File

@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
BRIDGE="$MOSAIC_HOME/tools/orchestrator-matrix/transport/matrix_transport.py"
if [[ ! -f "$BRIDGE" ]]; then
echo "[mosaic-orch-matrix] missing transport bridge: $BRIDGE" >&2
exit 1
fi
exec python3 "$BRIDGE" --repo "$(pwd)" --mode publish "$@"

View File

@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
CTRL="$MOSAIC_HOME/tools/orchestrator-matrix/controller/mosaic_orchestrator.py"
if [[ ! -f "$CTRL" ]]; then
echo "[mosaic-orchestrator] missing controller: $CTRL" >&2
exit 1
fi
exec python3 "$CTRL" --repo "$(pwd)" "$@"

View File

@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
SYNC="$MOSAIC_HOME/tools/orchestrator-matrix/controller/tasks_md_sync.py"
if [[ ! -f "$SYNC" ]]; then
echo "[mosaic-orchestrator-sync] missing sync script: $SYNC" >&2
exit 1
fi
exec python3 "$SYNC" --repo "$(pwd)" "$@"

View File

@@ -0,0 +1,218 @@
#!/usr/bin/env bash
set -euo pipefail
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
PROJECTS_FILE="$MOSAIC_HOME/projects.txt"
usage() {
cat <<USAGE
Usage: $(basename "$0") <command> [options]
Commands:
init
Create projects registry file at ~/.config/mosaic/projects.txt
add <repo-path> [repo-path...]
Add one or more repos to the registry
remove <repo-path> [repo-path...]
Remove one or more repos from the registry
list
Show registered repos
bootstrap [--all|repo-path...] [--force] [--quality-template <name>]
Bootstrap registered repos or explicit repo paths
orchestrate <drain|start|status|stop> [--all|repo-path...] [--poll-sec N] [--no-sync] [--worker-cmd "cmd"]
Run orchestrator actions across repos from one command
Examples:
mosaic-projects init
mosaic-projects add ~/src/syncagent ~/src/inventory-stickers
mosaic-projects bootstrap --all
mosaic-projects orchestrate drain --all --worker-cmd "codex -p"
mosaic-projects orchestrate start ~/src/syncagent --worker-cmd "opencode -p"
USAGE
}
ensure_registry() {
mkdir -p "$MOSAIC_HOME"
if [[ ! -f "$PROJECTS_FILE" ]]; then
cat > "$PROJECTS_FILE" <<EOF
# Mosaic managed projects (one absolute path per line)
# Lines starting with # are ignored.
EOF
fi
}
norm_path() {
local p="$1"
if [[ -d "$p" ]]; then
(cd "$p" && pwd)
else
return 1
fi
}
read_registry() {
ensure_registry
grep -vE '^\s*#|^\s*$' "$PROJECTS_FILE" | while read -r p; do
[[ -d "$p" ]] && echo "$p"
done
}
add_repo() {
local p="$1"
ensure_registry
local np
np="$(norm_path "$p")" || { echo "[mosaic-projects] skip missing dir: $p" >&2; return 1; }
if grep -Fxq "$np" "$PROJECTS_FILE"; then
echo "[mosaic-projects] already registered: $np"
return 0
fi
echo "$np" >> "$PROJECTS_FILE"
echo "[mosaic-projects] added: $np"
}
remove_repo() {
local p="$1"
ensure_registry
local np
np="$(norm_path "$p" 2>/dev/null || echo "$p")"
tmp="$(mktemp)"
grep -vFx "$np" "$PROJECTS_FILE" > "$tmp" || true
mv "$tmp" "$PROJECTS_FILE"
echo "[mosaic-projects] removed: $np"
}
resolve_targets() {
local use_all="$1"
shift
if [[ "$use_all" == "1" ]]; then
read_registry
return 0
fi
if [[ $# -gt 0 ]]; then
for p in "$@"; do
norm_path "$p" || { echo "[mosaic-projects] missing target: $p" >&2; exit 1; }
done
return 0
fi
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
git rev-parse --show-toplevel
return 0
fi
echo "[mosaic-projects] no targets provided. Use --all or pass repo paths." >&2
exit 1
}
cmd="${1:-}"
if [[ -z "$cmd" ]]; then
usage
exit 1
fi
shift || true
case "$cmd" in
init)
ensure_registry
echo "[mosaic-projects] registry ready: $PROJECTS_FILE"
;;
add)
[[ $# -gt 0 ]] || { echo "[mosaic-projects] add requires repo path(s)" >&2; exit 1; }
for p in "$@"; do add_repo "$p"; done
;;
remove)
[[ $# -gt 0 ]] || { echo "[mosaic-projects] remove requires repo path(s)" >&2; exit 1; }
for p in "$@"; do remove_repo "$p"; done
;;
list)
read_registry
;;
bootstrap)
use_all=0
force=0
quality_template=""
targets=()
while [[ $# -gt 0 ]]; do
case "$1" in
--all) use_all=1; shift ;;
--force) force=1; shift ;;
--quality-template) quality_template="${2:-}"; shift 2 ;;
*) targets+=("$1"); shift ;;
esac
done
mapfile -t repos < <(resolve_targets "$use_all" "${targets[@]}")
[[ ${#repos[@]} -gt 0 ]] || { echo "[mosaic-projects] no repos resolved"; exit 1; }
for repo in "${repos[@]}"; do
args=()
[[ $force -eq 1 ]] && args+=(--force)
[[ -n "$quality_template" ]] && args+=(--quality-template "$quality_template")
args+=("$repo")
echo "[mosaic-projects] bootstrap: $repo"
"$MOSAIC_HOME/bin/mosaic-bootstrap-repo" "${args[@]}"
add_repo "$repo" || true
done
;;
orchestrate)
action="${1:-}"
[[ -n "$action" ]] || { echo "[mosaic-projects] orchestrate requires action: drain|start|status|stop" >&2; exit 1; }
shift || true
use_all=0
poll_sec=15
no_sync=0
worker_cmd=""
targets=()
while [[ $# -gt 0 ]]; do
case "$1" in
--all) use_all=1; shift ;;
--poll-sec) poll_sec="${2:-15}"; shift 2 ;;
--no-sync) no_sync=1; shift ;;
--worker-cmd) worker_cmd="${2:-}"; shift 2 ;;
*) targets+=("$1"); shift ;;
esac
done
mapfile -t repos < <(resolve_targets "$use_all" "${targets[@]}")
[[ ${#repos[@]} -gt 0 ]] || { echo "[mosaic-projects] no repos resolved"; exit 1; }
for repo in "${repos[@]}"; do
echo "[mosaic-projects] orchestrate:$action -> $repo"
(
cd "$repo"
if [[ -n "$worker_cmd" ]]; then
export MOSAIC_WORKER_EXEC="$worker_cmd"
fi
if [[ -x "scripts/agent/orchestrator-daemon.sh" ]]; then
args=()
[[ "$action" == "start" || "$action" == "drain" ]] && args+=(--poll-sec "$poll_sec")
[[ $no_sync -eq 1 ]] && args+=(--no-sync)
bash scripts/agent/orchestrator-daemon.sh "$action" "${args[@]}"
else
case "$action" in
drain)
args=(--poll-sec "$poll_sec")
[[ $no_sync -eq 1 ]] && args+=(--no-sync)
"$MOSAIC_HOME/bin/mosaic-orchestrator-drain" "${args[@]}"
;;
status)
echo "[mosaic-projects] no daemon script in repo; run from bootstrapped repo or re-bootstrap"
;;
start|stop)
echo "[mosaic-projects] action '$action' requires scripts/agent/orchestrator-daemon.sh (run bootstrap first)" >&2
exit 1
;;
*)
echo "[mosaic-projects] unsupported action: $action" >&2
exit 1
;;
esac
fi
)
done
;;
*)
usage
exit 1
;;
esac

View File

@@ -0,0 +1,95 @@
#!/usr/bin/env bash
set -euo pipefail
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
RUNTIME="claude"
APPLY=0
usage() {
cat <<USAGE
Usage: $(basename "$0") [options]
Remove legacy runtime files that were preserved as *.mosaic-bak-* after Mosaic linking.
Only removes backups when the active file is a symlink to ~/.config/mosaic.
Options:
--runtime <name> Runtime to prune (default: claude)
--apply Perform deletions (default: dry-run)
-h, --help Show help
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--runtime)
[[ $# -lt 2 ]] && { echo "Missing value for --runtime" >&2; exit 1; }
RUNTIME="$2"
shift 2
;;
--apply)
APPLY=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
case "$RUNTIME" in
claude)
TARGET_ROOT="$HOME/.claude"
;;
*)
echo "Unsupported runtime: $RUNTIME" >&2
exit 1
;;
esac
if [[ ! -d "$TARGET_ROOT" ]]; then
echo "[mosaic-prune] Runtime directory not found: $TARGET_ROOT" >&2
exit 1
fi
mosaic_real="$(readlink -f "$MOSAIC_HOME")"
count_candidates=0
count_deletable=0
while IFS= read -r -d '' bak; do
count_candidates=$((count_candidates + 1))
base="${bak%%.mosaic-bak-*}"
if [[ ! -L "$base" ]]; then
continue
fi
base_real="$(readlink -f "$base" 2>/dev/null || true)"
if [[ -z "$base_real" ]]; then
continue
fi
if [[ "$base_real" != "$mosaic_real"/* ]]; then
continue
fi
count_deletable=$((count_deletable + 1))
if [[ $APPLY -eq 1 ]]; then
rm -rf "$bak"
echo "[mosaic-prune] deleted: $bak"
else
echo "[mosaic-prune] would delete: $bak"
fi
done < <(find "$TARGET_ROOT" \( -type f -o -type d \) -name '*.mosaic-bak-*' -print0)
if [[ $APPLY -eq 1 ]]; then
echo "[mosaic-prune] complete: deleted=$count_deletable candidates=$count_candidates runtime=$RUNTIME"
else
echo "[mosaic-prune] dry-run: deletable=$count_deletable candidates=$count_candidates runtime=$RUNTIME"
echo "[mosaic-prune] re-run with --apply to delete"
fi

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env bash
set -euo pipefail
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
TARGET_DIR="$(pwd)"
TEMPLATE=""
usage() {
cat <<USAGE
Usage: $(basename "$0") --template <name> [--target <dir>]
Apply Mosaic quality tools templates into a project.
Templates:
typescript-node
typescript-nextjs
monorepo
Examples:
$(basename "$0") --template typescript-node --target ~/src/my-project
$(basename "$0") --template monorepo
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--template)
TEMPLATE="${2:-}"
shift 2
;;
--target)
TARGET_DIR="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
if [[ -z "$TEMPLATE" ]]; then
echo "[mosaic-quality] Missing required --template" >&2
usage >&2
exit 1
fi
if [[ ! -d "$TARGET_DIR" ]]; then
echo "[mosaic-quality] Target directory does not exist: $TARGET_DIR" >&2
exit 1
fi
SCRIPT="$MOSAIC_HOME/tools/quality/scripts/install.sh"
if [[ ! -x "$SCRIPT" ]]; then
echo "[mosaic-quality] Missing install script: $SCRIPT" >&2
exit 1
fi
echo "[mosaic-quality] Applying template '$TEMPLATE' to $TARGET_DIR"
"$SCRIPT" --template "$TEMPLATE" --target "$TARGET_DIR"

View File

@@ -0,0 +1,52 @@
#!/usr/bin/env bash
set -euo pipefail
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
TARGET_DIR="$(pwd)"
usage() {
cat <<USAGE
Usage: $(basename "$0") [--target <dir>]
Run quality-rails verification checks inside a target repository.
Examples:
$(basename "$0")
$(basename "$0") --target ~/src/my-project
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--target)
TARGET_DIR="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
if [[ ! -d "$TARGET_DIR" ]]; then
echo "[mosaic-quality] Target directory does not exist: $TARGET_DIR" >&2
exit 1
fi
SCRIPT="$MOSAIC_HOME/tools/quality/scripts/verify.sh"
if [[ ! -x "$SCRIPT" ]]; then
echo "[mosaic-quality] Missing verify script: $SCRIPT" >&2
exit 1
fi
echo "[mosaic-quality] Running verification in $TARGET_DIR"
(
cd "$TARGET_DIR"
"$SCRIPT"
)

View File

@@ -0,0 +1,124 @@
#!/usr/bin/env bash
set -euo pipefail
# mosaic-release-upgrade — Upgrade installed Mosaic framework release.
#
# This re-runs the remote installer with explicit install mode controls.
# Default behavior is safe/idempotent (keep SOUL.md + memory).
#
# Usage:
# mosaic-release-upgrade
# mosaic-release-upgrade --ref main --keep
# mosaic-release-upgrade --ref v0.2.0 --overwrite --yes
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
REMOTE_SCRIPT_URL="${MOSAIC_REMOTE_INSTALL_URL:-https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.sh}"
BOOTSTRAP_REF="${MOSAIC_BOOTSTRAP_REF:-main}"
INSTALL_MODE="${MOSAIC_INSTALL_MODE:-keep}" # keep|overwrite
YES=false
DRY_RUN=false
usage() {
cat <<USAGE
Usage: $(basename "$0") [options]
Upgrade the installed Mosaic framework release.
Options:
--ref <name> Bootstrap archive ref (branch/tag/commit). Default: main
--keep Keep local files (SOUL.md, memory/) during upgrade (default)
--overwrite Overwrite target install directory contents
-y, --yes Skip confirmation prompt
--dry-run Show actions without executing
-h, --help Show this help
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--ref)
[[ $# -lt 2 ]] && { echo "Missing value for --ref" >&2; exit 1; }
BOOTSTRAP_REF="$2"
shift 2
;;
--keep)
INSTALL_MODE="keep"
shift
;;
--overwrite)
INSTALL_MODE="overwrite"
shift
;;
-y|--yes)
YES=true
shift
;;
--dry-run)
DRY_RUN=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
case "$INSTALL_MODE" in
keep|overwrite) ;;
*)
echo "[mosaic-release-upgrade] Invalid install mode: $INSTALL_MODE" >&2
exit 1
;;
esac
current_version="unknown"
if [[ -x "$MOSAIC_HOME/bin/mosaic" ]]; then
current_version="$("$MOSAIC_HOME/bin/mosaic" --version 2>/dev/null | awk '{print $2}' || true)"
[[ -n "$current_version" ]] || current_version="unknown"
fi
echo "[mosaic-release-upgrade] Current version: $current_version"
echo "[mosaic-release-upgrade] Target ref: $BOOTSTRAP_REF"
echo "[mosaic-release-upgrade] Install mode: $INSTALL_MODE"
echo "[mosaic-release-upgrade] Installer URL: $REMOTE_SCRIPT_URL"
if [[ "$DRY_RUN" == "true" ]]; then
echo "[mosaic-release-upgrade] Dry run: no changes applied."
exit 0
fi
if [[ "$YES" != "true" && -t 0 ]]; then
printf "Proceed with Mosaic release upgrade? [y/N]: "
read -r confirm
case "${confirm:-n}" in
y|Y|yes|YES) ;;
*)
echo "[mosaic-release-upgrade] Aborted."
exit 1
;;
esac
fi
if command -v curl >/dev/null 2>&1; then
curl -sL "$REMOTE_SCRIPT_URL" | \
MOSAIC_BOOTSTRAP_REF="$BOOTSTRAP_REF" \
MOSAIC_INSTALL_MODE="$INSTALL_MODE" \
MOSAIC_HOME="$MOSAIC_HOME" \
sh
elif command -v wget >/dev/null 2>&1; then
wget -qO- "$REMOTE_SCRIPT_URL" | \
MOSAIC_BOOTSTRAP_REF="$BOOTSTRAP_REF" \
MOSAIC_INSTALL_MODE="$INSTALL_MODE" \
MOSAIC_HOME="$MOSAIC_HOME" \
sh
else
echo "[mosaic-release-upgrade] ERROR: curl or wget required." >&2
exit 1
fi

View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ -x "scripts/agent/session-end.sh" ]]; then
exec bash scripts/agent/session-end.sh "$@"
fi
echo "[mosaic] Missing scripts/agent/session-end.sh in $(pwd)" >&2
exit 1

View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ -x "scripts/agent/session-start.sh" ]]; then
exec bash scripts/agent/session-start.sh
fi
echo "[mosaic] Missing scripts/agent/session-start.sh in $(pwd)" >&2
exit 1

View File

@@ -0,0 +1,183 @@
#!/usr/bin/env bash
set -euo pipefail
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
SKILLS_REPO_URL="${MOSAIC_SKILLS_REPO_URL:-https://git.mosaicstack.dev/mosaic/agent-skills.git}"
SKILLS_REPO_DIR="${MOSAIC_SKILLS_REPO_DIR:-$MOSAIC_HOME/sources/agent-skills}"
MOSAIC_SKILLS_DIR="$MOSAIC_HOME/skills"
MOSAIC_LOCAL_SKILLS_DIR="$MOSAIC_HOME/skills-local"
fetch=1
link_only=0
usage() {
cat <<USAGE
Usage: $(basename "$0") [options]
Sync canonical skills into ~/.config/mosaic/skills and link all Mosaic skills into runtime skill directories.
Options:
--link-only Skip git clone/pull and only relink from ~/.config/mosaic/{skills,skills-local}
--no-link Sync canonical skills but do not update runtime links
-h, --help Show help
Env:
MOSAIC_HOME Default: ~/.config/mosaic
MOSAIC_SKILLS_REPO_URL Default: https://git.mosaicstack.dev/mosaic/agent-skills.git
MOSAIC_SKILLS_REPO_DIR Default: ~/.config/mosaic/sources/agent-skills
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--link-only)
fetch=0
shift
;;
--no-link)
link_only=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
mkdir -p "$MOSAIC_HOME" "$MOSAIC_SKILLS_DIR" "$MOSAIC_LOCAL_SKILLS_DIR"
if [[ $fetch -eq 1 ]]; then
if [[ -d "$SKILLS_REPO_DIR/.git" ]]; then
echo "[mosaic-skills] Updating skills source: $SKILLS_REPO_DIR"
git -C "$SKILLS_REPO_DIR" pull --rebase
else
echo "[mosaic-skills] Cloning skills source to: $SKILLS_REPO_DIR"
mkdir -p "$(dirname "$SKILLS_REPO_DIR")"
git clone "$SKILLS_REPO_URL" "$SKILLS_REPO_DIR"
fi
SOURCE_SKILLS_DIR="$SKILLS_REPO_DIR/skills"
if [[ ! -d "$SOURCE_SKILLS_DIR" ]]; then
echo "[mosaic-skills] Missing source skills dir: $SOURCE_SKILLS_DIR" >&2
exit 1
fi
if command -v rsync >/dev/null 2>&1; then
rsync -a --delete "$SOURCE_SKILLS_DIR/" "$MOSAIC_SKILLS_DIR/"
else
rm -rf "$MOSAIC_SKILLS_DIR"/*
cp -R "$SOURCE_SKILLS_DIR"/* "$MOSAIC_SKILLS_DIR"/
fi
fi
if [[ ! -d "$MOSAIC_SKILLS_DIR" ]]; then
echo "[mosaic-skills] Canonical skills dir missing: $MOSAIC_SKILLS_DIR" >&2
exit 1
fi
if [[ $link_only -eq 1 ]]; then
echo "[mosaic-skills] Canonical sync completed (link update skipped)"
exit 0
fi
link_targets=(
"$HOME/.claude/skills"
"$HOME/.codex/skills"
"$HOME/.config/opencode/skills"
"$HOME/.pi/agent/skills"
)
canonical_real="$(readlink -f "$MOSAIC_SKILLS_DIR")"
link_skill_into_target() {
local skill_path="$1"
local target_dir="$2"
local name link_path
name="$(basename "$skill_path")"
# Do not distribute hidden/system skill directories globally.
if [[ "$name" == .* ]]; then
return
fi
link_path="$target_dir/$name"
if [[ -L "$link_path" ]]; then
ln -sfn "$skill_path" "$link_path"
return
fi
if [[ -e "$link_path" ]]; then
echo "[mosaic-skills] Preserve existing runtime-specific entry: $link_path"
return
fi
ln -s "$skill_path" "$link_path"
}
is_mosaic_skill_name() {
local name="$1"
# -d follows symlinks; -L catches broken symlinks that still indicate ownership
[[ -d "$MOSAIC_SKILLS_DIR/$name" || -L "$MOSAIC_SKILLS_DIR/$name" ]] && return 0
[[ -d "$MOSAIC_LOCAL_SKILLS_DIR/$name" || -L "$MOSAIC_LOCAL_SKILLS_DIR/$name" ]] && return 0
return 1
}
prune_stale_links_in_target() {
local target_dir="$1"
while IFS= read -r -d '' link_path; do
local name resolved
name="$(basename "$link_path")"
if is_mosaic_skill_name "$name"; then
continue
fi
resolved="$(readlink -f "$link_path" 2>/dev/null || true)"
if [[ -z "$resolved" ]]; then
rm -f "$link_path"
echo "[mosaic-skills] Removed stale broken skill link: $link_path"
continue
fi
if [[ "$resolved" == "$MOSAIC_HOME/"* ]]; then
rm -f "$link_path"
echo "[mosaic-skills] Removed stale retired skill link: $link_path"
fi
done < <(find "$target_dir" -mindepth 1 -maxdepth 1 -type l -print0)
}
for target in "${link_targets[@]}"; do
mkdir -p "$target"
# If target already resolves to canonical dir, skip to avoid self-link recursion/corruption.
target_real="$(readlink -f "$target" 2>/dev/null || true)"
if [[ -n "$target_real" && "$target_real" == "$canonical_real" ]]; then
echo "[mosaic-skills] Skip target (already canonical): $target"
continue
fi
prune_stale_links_in_target "$target"
while IFS= read -r -d '' skill; do
link_skill_into_target "$skill" "$target"
done < <(find "$MOSAIC_SKILLS_DIR" -mindepth 1 -maxdepth 1 -type d -print0)
if [[ -d "$MOSAIC_LOCAL_SKILLS_DIR" ]]; then
while IFS= read -r -d '' skill; do
link_skill_into_target "$skill" "$target"
done < <(find "$MOSAIC_LOCAL_SKILLS_DIR" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) -print0)
fi
echo "[mosaic-skills] Linked skills into: $target"
done
echo "[mosaic-skills] Complete"

View File

@@ -0,0 +1,218 @@
#!/usr/bin/env bash
set -euo pipefail
# mosaic-upgrade — Clean up stale per-project files after Mosaic centralization
#
# SOUL.md → Now global at ~/.config/mosaic/SOUL.md (remove from projects)
# CLAUDE.md → Now a thin pointer or removable (replace with pointer or remove)
# AGENTS.md → Keep project-specific content, strip stale load-order directives
#
# Usage:
# mosaic-upgrade [path] Upgrade a specific project (default: current dir)
# mosaic-upgrade --all Scan ~/src/* for projects to upgrade
# mosaic-upgrade --dry-run Show what would change without touching anything
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
# Colors (disabled if not a terminal)
if [[ -t 1 ]]; then
GREEN='\033[0;32m' YELLOW='\033[0;33m' RED='\033[0;31m'
CYAN='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' RESET='\033[0m'
else
GREEN='' YELLOW='' RED='' CYAN='' BOLD='' DIM='' RESET=''
fi
ok() { echo -e " ${GREEN}✓${RESET} $1"; }
skip() { echo -e " ${DIM}${RESET} $1"; }
warn() { echo -e " ${YELLOW}⚠${RESET} $1"; }
act() { echo -e " ${CYAN}→${RESET} $1"; }
DRY_RUN=false
ALL=false
TARGET=""
SEARCH_ROOT="${HOME}/src"
usage() {
cat <<USAGE
mosaic-upgrade — Clean up stale per-project files
Usage:
mosaic-upgrade [path] Upgrade a specific project (default: cwd)
mosaic-upgrade --all Scan ~/src/* for all git projects
mosaic-upgrade --dry-run Preview changes without writing
mosaic-upgrade --all --dry-run Preview all projects
After Mosaic centralization:
SOUL.md → Removed (now global at ~/.config/mosaic/SOUL.md)
CLAUDE.md → Replaced with thin pointer or removed
AGENTS.md → Stale load-order sections stripped; project content preserved
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=true; shift ;;
--all) ALL=true; shift ;;
--root) SEARCH_ROOT="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
-*) echo "Unknown flag: $1" >&2; usage >&2; exit 1 ;;
*) TARGET="$1"; shift ;;
esac
done
# Generate the thin CLAUDE.md pointer
CLAUDE_POINTER='# CLAUDE Compatibility Pointer
This file exists so Claude Code sessions load Mosaic standards.
## MANDATORY — Read Before Any Response
BEFORE responding to any user message, READ `~/.config/mosaic/AGENTS.md`.
That file is the universal agent configuration. Do NOT respond until you have loaded it.
Then read the project-local `AGENTS.md` in this repository for project-specific guidance.'
upgrade_project() {
local project_dir="$1"
local project_name
project_name="$(basename "$project_dir")"
local changed=false
echo -e "\n${BOLD}$project_name${RESET} ${DIM}($project_dir)${RESET}"
# ── SOUL.md ──────────────────────────────────────────────
local soul="$project_dir/SOUL.md"
if [[ -f "$soul" ]]; then
if [[ "$DRY_RUN" == "true" ]]; then
act "Would remove SOUL.md (now global at ~/.config/mosaic/SOUL.md)"
else
rm "$soul"
ok "Removed SOUL.md (now global)"
fi
changed=true
else
skip "No SOUL.md (already clean)"
fi
# ── CLAUDE.md ────────────────────────────────────────────
local claude_md="$project_dir/CLAUDE.md"
if [[ -f "$claude_md" ]]; then
local claude_content
claude_content="$(cat "$claude_md")"
# Check if it's already a thin pointer to AGENTS.md
if echo "$claude_content" | grep -q "READ.*~/.config/mosaic/AGENTS.md"; then
skip "CLAUDE.md already points to global AGENTS.md"
else
if [[ "$DRY_RUN" == "true" ]]; then
act "Would replace CLAUDE.md with thin pointer to global AGENTS.md"
else
# Back up the original
cp "$claude_md" "${claude_md}.mosaic-bak"
echo "$CLAUDE_POINTER" > "$claude_md"
ok "Replaced CLAUDE.md with pointer (backup: CLAUDE.md.mosaic-bak)"
fi
changed=true
fi
else
skip "No CLAUDE.md"
fi
# ── AGENTS.md (strip stale load-order, preserve project content) ─
local agents="$project_dir/AGENTS.md"
if [[ -f "$agents" ]]; then
# Detect stale load-order patterns
local has_stale=false
# Pattern 1: References to SOUL.md in load order
if grep -qE "(Read|READ|Load).*SOUL\.md" "$agents" 2>/dev/null; then
has_stale=true
fi
# Pattern 2: Old "## Load Order" section that references centralized files
if grep -q "## Load Order" "$agents" 2>/dev/null && \
grep -qE "STANDARDS\.md|SOUL\.md" "$agents" 2>/dev/null; then
has_stale=true
fi
# Pattern 3: Old ~/.mosaic/ path (pre-centralization)
if grep -q '~/.mosaic/' "$agents" 2>/dev/null; then
has_stale=true
fi
if [[ "$has_stale" == "true" ]]; then
if [[ "$DRY_RUN" == "true" ]]; then
act "Would strip stale load-order section from AGENTS.md"
# Show what we detect
if grep -qn "## Load Order" "$agents" 2>/dev/null; then
local line
line=$(grep -n "## Load Order" "$agents" | head -1 | cut -d: -f1)
echo -e " ${DIM}Line $line: Found '## Load Order' section referencing SOUL.md/STANDARDS.md${RESET}"
fi
if grep -qn '~/.mosaic/' "$agents" 2>/dev/null; then
echo -e " ${DIM}Found references to old ~/.mosaic/ path${RESET}"
fi
else
cp "$agents" "${agents}.mosaic-bak"
# Strip the Load Order section (from "## Load Order" to next "##" or "---")
if grep -q "## Load Order" "$agents"; then
awk '
/^## Load Order/ { skip=1; next }
skip && /^(## |---)/ { skip=0 }
skip { next }
{ print }
' "${agents}.mosaic-bak" > "$agents"
fi
# Fix old ~/.mosaic/ → ~/.config/mosaic/
if grep -q '~/.mosaic/' "$agents"; then
sed -i 's|~/.mosaic/|~/.config/mosaic/|g' "$agents"
fi
ok "Stripped stale load-order from AGENTS.md (backup: AGENTS.md.mosaic-bak)"
fi
changed=true
else
skip "AGENTS.md has no stale directives"
fi
else
skip "No AGENTS.md"
fi
# ── .claude/settings.json (leave alone) ──────────────────
# Project-specific settings are fine — don't touch them.
if [[ "$changed" == "false" ]]; then
echo -e " ${GREEN}Already up to date.${RESET}"
fi
}
# ── Main ───────────────────────────────────────────────────
if [[ "$DRY_RUN" == "true" ]]; then
echo -e "${BOLD}Mode: DRY RUN (no files will be changed)${RESET}"
fi
if [[ "$ALL" == "true" ]]; then
echo -e "${BOLD}Scanning $SEARCH_ROOT for projects...${RESET}"
count=0
for dir in "$SEARCH_ROOT"/*/; do
[[ -d "$dir/.git" ]] || continue
upgrade_project "$dir"
count=$((count + 1))
done
echo -e "\n${BOLD}Scanned $count projects.${RESET}"
elif [[ -n "$TARGET" ]]; then
if [[ ! -d "$TARGET" ]]; then
echo "[mosaic-upgrade] ERROR: $TARGET is not a directory." >&2
exit 1
fi
upgrade_project "$TARGET"
else
upgrade_project "$(pwd)"
fi
if [[ "$DRY_RUN" == "true" ]]; then
echo -e "\n${YELLOW}This was a dry run. Run without --dry-run to apply changes.${RESET}"
fi

View File

@@ -0,0 +1,116 @@
#!/usr/bin/env bash
set -euo pipefail
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
BOOTSTRAP_CMD="$MOSAIC_HOME/bin/mosaic-bootstrap-repo"
roots=("$HOME/src")
apply=0
usage() {
cat <<USAGE
Usage: $(basename "$0") [options]
Upgrade all Mosaic-linked slave repositories by re-running repo bootstrap with --force.
Options:
--root <path> Add a search root (repeatable). Default: $HOME/src
--apply Execute upgrades. Without this flag, script is dry-run.
-h, --help Show this help
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--root)
[[ $# -lt 2 ]] && { echo "Missing value for --root" >&2; exit 1; }
roots+=("$2")
shift 2
;;
--apply)
apply=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
if [[ ! -x "$BOOTSTRAP_CMD" ]]; then
echo "[mosaic-upgrade] Missing bootstrap command: $BOOTSTRAP_CMD" >&2
echo "[mosaic-upgrade] Install/refresh framework first: ~/.config/mosaic/install.sh" >&2
exit 1
fi
# De-duplicate roots while preserving order.
uniq_roots=()
for r in "${roots[@]}"; do
skip=0
for e in "${uniq_roots[@]}"; do
[[ "$r" == "$e" ]] && { skip=1; break; }
done
[[ $skip -eq 0 ]] && uniq_roots+=("$r")
done
candidates=()
for root in "${uniq_roots[@]}"; do
[[ -d "$root" ]] || continue
while IFS= read -r marker; do
repo_dir="$(dirname "$(dirname "$marker")")"
if [[ -d "$repo_dir/.git" ]]; then
candidates+=("$repo_dir")
fi
done < <(find "$root" -type f -path '*/.mosaic/README.md' 2>/dev/null)
done
# De-duplicate repos while preserving order.
repos=()
for repo in "${candidates[@]}"; do
skip=0
for existing in "${repos[@]}"; do
[[ "$repo" == "$existing" ]] && { skip=1; break; }
done
[[ $skip -eq 0 ]] && repos+=("$repo")
done
count_total=${#repos[@]}
count_ok=0
count_fail=0
mode="DRY-RUN"
[[ $apply -eq 1 ]] && mode="APPLY"
echo "[mosaic-upgrade] Mode: $mode"
echo "[mosaic-upgrade] Roots: ${uniq_roots[*]}"
echo "[mosaic-upgrade] Linked repos found: $count_total"
if [[ $count_total -eq 0 ]]; then
exit 0
fi
for repo in "${repos[@]}"; do
if [[ $apply -eq 1 ]]; then
if "$BOOTSTRAP_CMD" "$repo" --force >/dev/null; then
echo "[mosaic-upgrade] upgraded: $repo"
count_ok=$((count_ok + 1))
else
echo "[mosaic-upgrade] FAILED: $repo" >&2
count_fail=$((count_fail + 1))
fi
else
echo "[mosaic-upgrade] would upgrade: $repo"
fi
done
if [[ $apply -eq 1 ]]; then
echo "[mosaic-upgrade] complete: ok=$count_ok failed=$count_fail total=$count_total"
[[ $count_fail -gt 0 ]] && exit 1
fi

View File

@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
# mosaic-wizard — Thin shell wrapper for the bundled TypeScript wizard
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
# Look for the bundle in the installed location first, then the source repo
WIZARD_BIN="$MOSAIC_HOME/dist/mosaic-wizard.mjs"
if [[ ! -f "$WIZARD_BIN" ]]; then
WIZARD_BIN="$(cd "$(dirname "$0")/.." && pwd)/dist/mosaic-wizard.mjs"
fi
if [[ ! -f "$WIZARD_BIN" ]]; then
echo "[mosaic-wizard] ERROR: Wizard bundle not found." >&2
echo "[mosaic-wizard] Run 'pnpm build' in the mosaic-bootstrap repo, or re-install Mosaic." >&2
exit 1
fi
if ! command -v node >/dev/null 2>&1; then
echo "[mosaic-wizard] ERROR: Node.js is required but not found." >&2
echo "[mosaic-wizard] Install Node.js 18+ from https://nodejs.org" >&2
exit 1
fi
exec node "$WIZARD_BIN" "$@"