Files
stack/packages/mosaic/framework/tools/_scripts/mosaic-init
Jarvis 361fece023
Some checks failed
ci/woodpecker/pr/ci Pipeline failed
ci/woodpecker/push/ci Pipeline failed
fix: make mosaic init idempotent — detect existing config files
- mosaic-init bash script: detect existing SOUL.md/USER.md/TOOLS.md and
  prompt user to keep, import (re-use values as defaults), or overwrite.
  Non-interactive mode exits cleanly unless --force is passed.
  Overwrite creates timestamped backups before replacing files.

- launch.ts checkSoul(): prefer 'mosaic wizard' over legacy bash script
  when SOUL.md is missing, with fallback to mosaic-init.

- detect-install.ts: pre-populate wizard state with existing values when
  user chooses 'reconfigure', so they see current settings as defaults.

- soul-setup.ts: show existing agent name and communication style as
  defaults during reconfiguration.

- Added tests for reconfigure pre-population and reset non-population.
2026-04-02 20:20:59 -05:00

556 lines
18 KiB
Bash
Executable File

#!/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
--force Overwrite existing files without prompting
-h, --help Show help
USAGE
}
NON_INTERACTIVE=0
SOUL_ONLY=0
FORCE=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 ;;
--force) FORCE=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\""
}
# ── Existing file detection ────────────────────────────────────
detect_existing_config() {
local found=0
local existing_files=()
[[ -f "$SOUL_OUTPUT" ]] && { found=1; existing_files+=("SOUL.md"); }
[[ -f "$USER_OUTPUT" ]] && { found=1; existing_files+=("USER.md"); }
[[ -f "$TOOLS_OUTPUT" ]] && { found=1; existing_files+=("TOOLS.md"); }
if [[ $found -eq 0 || $FORCE -eq 1 ]]; then
return 0 # No existing files or --force: proceed with fresh install
fi
echo "[mosaic-init] Existing configuration detected:"
for f in "${existing_files[@]}"; do
echo "$f"
done
# Show current agent name if SOUL.md exists
if [[ -f "$SOUL_OUTPUT" ]]; then
local current_name
current_name=$(grep -oP 'You are \*\*\K[^*]+' "$SOUL_OUTPUT" 2>/dev/null || true)
if [[ -n "$current_name" ]]; then
echo " Agent: $current_name"
fi
fi
echo ""
if [[ $NON_INTERACTIVE -eq 1 ]]; then
echo "[mosaic-init] Existing config found. Use --force to overwrite in non-interactive mode."
exit 0
fi
echo "What would you like to do?"
echo " 1) keep — Keep existing files, skip init (default)"
echo " 2) import — Import values from existing files as defaults, then regenerate"
echo " 3) overwrite — Start fresh, overwrite all files"
printf "Choose [1/2/3]: "
read -r choice
case "${choice:-1}" in
1|keep)
echo "[mosaic-init] Keeping existing configuration."
# Still push to runtime adapters in case framework was updated
if [[ -x "$MOSAIC_HOME/tools/_scripts/mosaic-link-runtime-assets" ]]; then
echo "[mosaic-init] Updating runtime adapters..."
"$MOSAIC_HOME/tools/_scripts/mosaic-link-runtime-assets"
fi
echo "[mosaic-init] Done. Launch with: mosaic claude"
exit 0
;;
2|import)
echo "[mosaic-init] Importing values from existing files as defaults..."
import_existing_values
;;
3|overwrite)
echo "[mosaic-init] Starting fresh install..."
# Back up existing files
local ts
ts=$(date +%Y%m%d%H%M%S)
for f in "${existing_files[@]}"; do
local src="$MOSAIC_HOME/$f"
if [[ -f "$src" ]]; then
cp "$src" "${src}.bak.${ts}"
echo " Backed up $f${f}.bak.${ts}"
fi
done
;;
*)
echo "[mosaic-init] Invalid choice. Keeping existing configuration."
exit 0
;;
esac
}
import_existing_values() {
# Import SOUL.md values
if [[ -f "$SOUL_OUTPUT" ]]; then
local content
content=$(cat "$SOUL_OUTPUT")
if [[ -z "$AGENT_NAME" ]]; then
AGENT_NAME=$(echo "$content" | grep -oP 'You are \*\*\K[^*]+' 2>/dev/null || true)
fi
if [[ -z "$ROLE_DESCRIPTION" ]]; then
ROLE_DESCRIPTION=$(echo "$content" | grep -oP 'Role identity: \K.+' 2>/dev/null || true)
fi
if [[ -z "$STYLE" ]]; then
if echo "$content" | grep -q 'Be direct, concise'; then
STYLE="direct"
elif echo "$content" | grep -q 'Be warm and conversational'; then
STYLE="friendly"
elif echo "$content" | grep -q 'Use professional, structured'; then
STYLE="formal"
fi
fi
fi
# Import USER.md values
if [[ -f "$USER_OUTPUT" ]]; then
local content
content=$(cat "$USER_OUTPUT")
if [[ -z "$USER_NAME" ]]; then
USER_NAME=$(echo "$content" | grep -oP '\*\*Name:\*\* \K.+' 2>/dev/null || true)
fi
if [[ -z "$PRONOUNS" ]]; then
PRONOUNS=$(echo "$content" | grep -oP '\*\*Pronouns:\*\* \K.+' 2>/dev/null || true)
fi
if [[ -z "$TIMEZONE" ]]; then
TIMEZONE=$(echo "$content" | grep -oP '\*\*Timezone:\*\* \K.+' 2>/dev/null || true)
fi
fi
# Import TOOLS.md values
if [[ -f "$TOOLS_OUTPUT" ]]; then
local content
content=$(cat "$TOOLS_OUTPUT")
if [[ -z "$CREDENTIALS_LOCATION" ]]; then
CREDENTIALS_LOCATION=$(echo "$content" | grep -oP '\*\*Location:\*\* \K.+' 2>/dev/null || true)
fi
fi
}
detect_existing_config
# ── 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/tools/_scripts/mosaic-link-runtime-assets" ]]; then
echo "[mosaic-init] Updating runtime adapters..."
"$MOSAIC_HOME/tools/_scripts/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/tools/_scripts/mosaic-link-runtime-assets" ]]; then
echo ""
echo "[mosaic-init] Updating runtime adapters..."
"$MOSAIC_HOME/tools/_scripts/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."