Skill: jeadland/council-brief (version 2.0.0)

--- name: council-brief version: 2.0.0 description: | Unified LLM Council skill — install, query, and manage the multi-model consensus app. Get synthesized answers from multiple LLMs via quick CLI or full web UI. One skill does it all. slash_command: /council-brief metadata: {"category":"productivity","tags":["llm","council","llm-council","consensus","multi-model","installer","query","unified"],"repo":"https://github.com/jeadland/llm-council"} --- # Council Brief v2.0.0 (Unified) **One skill to rule them all** — install, query, and manage LLM Council. LLM Council is a multi-model consensus app: ask one question to many models, let them critique each other, get a synthesized chairman answer. > **v2.0.0 Upgrade:** Now unified! Previously separate `install-llm-council` and `ask-council` skills are merged here. One command handles everything. ## Usage | Command | Action | |---------|--------| | `/council-brief install` | Install deps and start backend + frontend | | `/council-brief ask <question>` | Quick chairman's answer (headless) | | `/council-brief <question>` | Shorthand — just ask directly | | `/council-brief status` | Check if services are running | | `/council-brief stop` | Stop background services | | `/council-brief --help` | Show usage help | ## Examples **Install and run:** ``` /council-brief install ``` **Quick question (no browser needed):** ``` /council-brief ask "Should I invest in Tesla right now?" /council-brief "Is Python or Go better for microservices?" /council-brief What are the tradeoffs of Rust vs Zig? ``` **Check status:** ``` /council-brief status ``` **Stop services:** ``` /council-brief stop ``` ## Two Ways to Use | Mode | Best For | Command | |------|----------|---------| | **Quick answer** | Fast decisions, mobile, casual questions | `/council-brief "question"` | | **Full discussion** | Deep research, exploring disagreements, seeing all model responses | `/council-brief install` then open browser at `:5173` | ## How Ask Mode Works 1. Sends your question to the LLM Council backend 2. Waits for Stage 1 (all models respond) 3. Waits for Stage 2 (models rank each other) 4. Returns Stage 3 (Chairman's final synthesis) 5. **Takes 30-60 seconds** — models need time to deliberate ## How Install Mode Works 1. Resolves credentials (env → workspace `.env` → OpenClaw gateway) 2. Clones/updates `https://github.com/jeadland/llm-council` 3. Installs Python backend (`uv sync`) 4. Installs frontend (`npm ci`) 5. Starts backend (FastAPI on `:8001`) 6. Starts frontend (Vite dev on `:5173`) ## Prerequisites - `git`, `uv` (Python), `npm`, `jq` - API credentials: `OPENROUTER_API_KEY` in env/workspace `.env`, OR OpenClaw gateway configured ## Ports | Service | Port | |---------|------| | Backend (FastAPI) | 8001 | | Frontend dev | 5173 | | Frontend preview | 4173 | ## Files | File | Purpose | |------|---------| | `SKILL.md` | This documentation | | `council-brief.sh` | Main router script (install/ask/status/stop) | | `ask-council.sh` | Query handler (chairman's answer) | | `install.sh` | Installer/launcher | | `stop.sh` | Service stopper | | `status.sh` | Service status checker | | `_meta.json` | Skill metadata | ## Agent Instructions When user says `/council-brief <args>`: ```bash bash ~/.openclaw/skills/council-brief/council-brief.sh "$@" ``` The `council-brief.sh` script handles argument routing: - `install` → runs `install.sh` (setup + launch) - `ask <question>` or any freeform text → runs `ask-council.sh` (query mode) - `status` → runs `status.sh` (check services) - `stop` → runs `stop.sh` (stop services) - `--help` or `help` → shows usage ## Changelog **v2.0.0** — Unified skill merging: - `install-llm-council` (setup/launcher) - `council-brief` v1.x (quick answers only) - `ask-council` (deprecated duplicate) Now one skill handles everything: install, query, status, stop. **v1.0.5** — Previous version (query-only mode, required manual install)

path: _meta.json
content: name: council-brief
version: 2.0.0

path: ask-council.sh
content: #!/usr/bin/env bash
# ask-council.sh — Quick headless access to LLM Council
# Usage: ask-council.sh "Your question here"
#
# Returns: Chairman's synthesized answer (or error message)

set -euo pipefail

API_BASE="http://127.0.0.1:8001"
TIMEOUT_SEC=120

# ── Colors ────────────────────────────────────────────────────────────────────
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
error() { echo -e "${RED}[council-brief]${NC} $*" >&2; exit 1; }
info()  { echo -e "${GREEN}[council-brief]${NC} $*"; }
warn()  { echo -e "${YELLOW}[council-brief]${NC} $*"; }

# ── Check input ───────────────────────────────────────────────────────────────
QUESTION="${1:-}"
if [[ -z "$QUESTION" ]]; then
  error "Usage: ask-council.sh 'Your question here'"
fi

# ── Check if backend is running ───────────────────────────────────────────────
if ! curl -s "$API_BASE/" > /dev/null 2>&1; then
  error "LLM Council backend not running. Start it first:\n  /council-brief install"
fi

# ── Create conversation ───────────────────────────────────────────────────────
CONVO_RESPONSE=$(curl -s -X POST "$API_BASE/api/conversations" -H "Content-Type: application/json" -d '{}')
CONVO_ID=$(echo "$CONVO_RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin)['id'])")
SHORT_ID=$(echo "$CONVO_RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin).get('short_id', ''))")
if [[ -z "$CONVO_ID" ]]; then
  error "Failed to create conversation"
fi

# ── Start council run ─────────────────────────────────────────────────────────
RUN_RESPONSE=$(curl -s -X POST "$API_BASE/api/conversations/$CONVO_ID/runs" \
  -H "Content-Type: application/json" \
  -d "{\"content\": $(echo "$QUESTION" | python3 -c 'import json, sys; print(json.dumps(sys.stdin.read().strip()))')}")
RUN_ID=$(echo "$RUN_RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin)['run_id'])")
if [[ -z "$RUN_ID" ]]; then
  error "Failed to start council run"
fi

info "Council is deliberating... (this may take 30-60s)"

# ── Poll until complete ───────────────────────────────────────────────────────
START_TIME=$(date +%s)
while true; do
  NOW=$(date +%s)
  ELAPSED=$((NOW - START_TIME))
  
  if [[ $ELAPSED -gt $TIMEOUT_SEC ]]; then
    error "Timed out after ${TIMEOUT_SEC}s. The council is still talking."
  fi

  STATUS=$(curl -s "$API_BASE/api/conversations/$CONVO_ID/runs/$RUN_ID")
  RUN_STATE=$(echo "$STATUS" | python3 -c "import sys, json; print(json.load(sys.stdin).get('status', 'unknown'))")
  
  case "$RUN_STATE" in
    complete)
      break
      ;;
    failed|canceled)
      ERROR=$(echo "$STATUS" | python3 -c "import sys, json; print(json.load(sys.stdin).get('error', 'Unknown error'))")
      error "Council session failed: $ERROR"
      ;;
    *)
      sleep 2
      echo -n "."
      ;;
  esac
done

echo ""  # newline after dots

# ── Extract chairman answer ───────────────────────────────────────────────────
CHAIRMAN_ANSWER=$(echo "$STATUS" | python3 -c "
import sys, json
data = json.load(sys.stdin)
stage3 = data.get('stage3', {})
if stage3.get('status') == 'complete':
    result = stage3.get('data', {})
    print(result.get('response', 'No answer found'))
else:
    print('Chairman did not complete synthesis')")

# ── Output ────────────────────────────────────────────────────────────────────
LOCAL_IP="$(hostname -I | awk '{print $1}')"

echo ""
echo "═══════════════════════════════════════════════════════════════"
echo "                    CHAIRMAN'S ANSWER"
echo "═══════════════════════════════════════════════════════════════"
echo ""
echo "$CHAIRMAN_ANSWER"
echo ""
echo "═══════════════════════════════════════════════════════════════"
echo ""
if [[ -n "$SHORT_ID" ]]; then
  echo "Short ID: $SHORT_ID"
  echo ""
  echo "View full discussion: http://${LOCAL_IP}:5173/c/${SHORT_ID}"
else
  echo "Conversation ID: $CONVO_ID"
  echo ""
  echo "View full discussion: http://${LOCAL_IP}:5173"
fi
echo ""


path: council-brief.sh
content: #!/usr/bin/env bash
# council-brief.sh — Unified router for LLM Council skill
# Usage: council-brief.sh [install|ask <question>|status|stop|<question>|--help]
#
# Routes commands to appropriate handler:
#   install  → install.sh (setup + launch)
#   ask      → ask-council.sh (headless council query)
#   status   → status.sh (check services)
#   stop     → stop.sh (stop services)
#   <text>   → treated as question for ask mode
#   --help   → usage

set -euo pipefail

SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# ── Colors ────────────────────────────────────────────────────────────────────
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
error() { echo -e "${RED}[council-brief]${NC} $*" >&2; exit 1; }
info()  { echo -e "${GREEN}[council-brief]${NC} $*"; }
warn()  { echo -e "${YELLOW}[council-brief]${NC} $*"; }

# ── Show usage ──────────────────────────────────────────────────────────────
usage() {
  cat <<EOF
Council Brief v2.0.0 — Unified LLM Council

Usage: /council-brief [install|ask <question>|status|stop|<question>]

Commands:
  install          Install dependencies and start services
  ask <question>   Ask the council a question (headless)
  status           Check if services are running
  stop             Stop background services
  --help           Show this help
  <question>       Any other text is treated as a question

Examples:
  /council-brief install
  /council-brief ask "Should I invest in Tesla?"
  /council-brief "Is Python or Go better for microservices?"
  /council-brief status
  /council-brief stop

Two ways to use:
  • Quick answer: /council-brief "your question"
  • Full web UI:  /council-brief install → open browser at :5173
EOF
}

# ── Parse arguments ───────────────────────────────────────────────────────────
if [[ $# -eq 0 ]]; then
  usage
  exit 0
fi

CMD="${1:-}"
shift || true

# ── Route to appropriate handler ──────────────────────────────────────────────
case "$CMD" in
  install)
    bash "$SKILL_DIR/install.sh" "$@"
    ;;
  
  status)
    bash "$SKILL_DIR/status.sh" "$@"
    ;;
  
  stop)
    bash "$SKILL_DIR/stop.sh" "$@"
    ;;
  
  ask)
    # "ask" keyword with question following
    QUESTION="${*:-}"
    if [[ -z "$QUESTION" ]]; then
      error "Usage: /council-brief ask '<question>'"
    fi
    bash "$SKILL_DIR/ask-council.sh" "$QUESTION"
    ;;
  
  help|--help|-h)
    usage
    exit 0
    ;;
  
  *)
    # Any other text — treat as a question
    # Reconstruct the full input including the first "arg"
    QUESTION="$CMD ${*:-}"
    QUESTION="${QUESTION%% }"  # trim trailing space if no args
    bash "$SKILL_DIR/ask-council.sh" "$QUESTION"
    ;;
esac


path: install.sh
content: #!/usr/bin/env bash
# install.sh — One-shot installer & launcher for LLM Council
# Usage: install.sh [--mode dev|preview] [--dir PATH]
# OpenClaw-native: reads credentials from OpenClaw config — no API key prompt.

set -euo pipefail

# ── Defaults ──────────────────────────────────────────────────────────────────
MODE="dev"
INSTALL_DIR="${HOME}/workspace/llm-council"
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PID_FILE="${SKILL_DIR}/pids"
WORKSPACE_ENV="${HOME}/.openclaw/workspace/.env"
OPENCLAW_CONFIG="${HOME}/.openclaw/openclaw.json"

# ── Arg parsing ───────────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
  case "$1" in
    --mode) MODE="$2"; shift 2 ;;
    --dir) INSTALL_DIR="$2"; shift 2 ;;
    *) echo "Unknown flag: $1" >&2; exit 1 ;;
  esac
done

# ── Colors ────────────────────────────────────────────────────────────────────
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
info()  { echo -e "${GREEN}[council-brief]${NC} $*"; }
warn()  { echo -e "${YELLOW}[council-brief]${NC} $*"; }
error() { echo -e "${RED}[council-brief] ERROR:${NC} $*" >&2; exit 1; }

# ── Resolve credentials — OpenClaw-native (no interactive prompt) ─────────────
#
# Priority order:
#   1. Environment: OPENROUTER_API_KEY (already exported)
#   2. Workspace .env: ~/.openclaw/workspace/.env
#   3. OpenClaw local gateway: ~/.openclaw/openclaw.json → gateway.auth.token
#      Uses http://127.0.0.1:<port>/v1/chat/completions as drop-in OpenAI API
#
API_MODE="openrouter"  # or "openclaw_gateway"
OPENROUTER_API_KEY="${OPENROUTER_API_KEY:-}"
OPENCLAW_GATEWAY_TOKEN=""
OPENCLAW_GATEWAY_PORT=""

# Try workspace .env
if [[ -z "$OPENROUTER_API_KEY" && -f "$WORKSPACE_ENV" ]]; then
  OPENROUTER_API_KEY="$(grep -E '^OPENROUTER_API_KEY=' "$WORKSPACE_ENV" \
    | cut -d= -f2- | tr -d '"' | tr -d "'" | head -1 || true)"
fi

# Try OpenClaw local gateway as fallback
if [[ -z "$OPENROUTER_API_KEY" ]] && command -v jq &>/dev/null && [[ -f "$OPENCLAW_CONFIG" ]]; then
  OPENCLAW_GATEWAY_TOKEN="$(jq -r '.gateway.auth.token // empty' "$OPENCLAW_CONFIG" 2>/dev/null || true)"
  OPENCLAW_GATEWAY_PORT="$(jq -r '.gateway.port // 18789' "$OPENCLAW_CONFIG" 2>/dev/null || true)"
  if [[ -n "$OPENCLAW_GATEWAY_TOKEN" ]]; then
    API_MODE="openclaw_gateway"
    info "No OPENROUTER_API_KEY found — using OpenClaw local gateway on :${OPENCLAW_GATEWAY_PORT}"
  fi
fi

if [[ -z "$OPENROUTER_API_KEY" && "$API_MODE" != "openclaw_gateway" ]]; then
  error "No API credentials found. Set OPENROUTER_API_KEY in environment or ~/.openclaw/workspace/.env, or ensure OpenClaw gateway is configured in ${OPENCLAW_CONFIG}"
fi

if [[ "$API_MODE" == "openrouter" ]]; then
  info "API: OpenRouter (key: ${#OPENROUTER_API_KEY} chars)"
else
  info "API: OpenClaw local gateway (port ${OPENCLAW_GATEWAY_PORT})"
fi

# ── Check prerequisites ───────────────────────────────────────────────────────
for cmd in git uv npm; do
  command -v "$cmd" &>/dev/null || error "Required tool not found: $cmd"
done

# ── Clone or update repo ──────────────────────────────────────────────────────
if [[ -d "$INSTALL_DIR/.git" ]]; then
  info "Repo exists at $INSTALL_DIR — pulling latest..."
  git -C "$INSTALL_DIR" pull --ff-only 2>&1 | tail -1
else
  info "Cloning llm-council to $INSTALL_DIR..."
  mkdir -p "$(dirname "$INSTALL_DIR")"
  git clone https://github.com/jeadland/llm-council.git "$INSTALL_DIR"
fi

cd "$INSTALL_DIR"

# ── Write .env ────────────────────────────────────────────────────────────────
info "Writing .env..."
if [[ "$API_MODE" == "openclaw_gateway" ]]; then
  # Use local OpenClaw gateway — OpenAI-compatible endpoint, no external key needed
  cat > .env <<EOF
# OpenClaw local gateway — no external API key required
OPENROUTER_API_KEY=${OPENCLAW_GATEWAY_TOKEN}
OPENROUTER_API_URL=http://127.0.0.1:${OPENCLAW_GATEWAY_PORT}/v1/chat/completions
EOF
  info ".env: OpenClaw gateway mode (http://127.0.0.1:${OPENCLAW_GATEWAY_PORT}/v1/chat/completions)"
else
  cat > .env <<EOF
OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
EOF
  info ".env: OpenRouter direct mode"
fi

# ── Backend: uv sync ──────────────────────────────────────────────────────────
info "Running uv sync (Python backend)..."
uv sync 2>&1 | tail -5

# ── Frontend: npm ci ──────────────────────────────────────────────────────────
info "Running npm ci (frontend)..."
cd frontend
npm ci --silent
cd ..

# ── Stop any existing services ────────────────────────────────────────────────
if [[ -f "$PID_FILE" ]]; then
  warn "Stopping existing services..."
  while IFS= read -r pid; do
    [[ -z "$pid" ]] && continue
    kill "$pid" 2>/dev/null && info "  Killed PID $pid" || true
  done < "$PID_FILE"
  rm -f "$PID_FILE"
  sleep 1  # give OS a moment to release ports
fi

# ── Build frontend (preview mode only) ───────────────────────────────────────
if [[ "$MODE" == "preview" ]]; then
  info "Building frontend for preview..."
  cd frontend
  npm run build 2>&1 | tail -5
  cd ..
fi

# ── Kill any stale listeners on ports we need ─────────────────────────────────
PORTS_TO_FREE=(8001 5173 4173)
for port in "${PORTS_TO_FREE[@]}"; do
  STALE="$(lsof -nP -iTCP:${port} -sTCP:LISTEN -t 2>/dev/null || true)"
  if [[ -n "$STALE" ]]; then
    warn "Port ${port} in use by PID(s) $STALE — killing..."
    echo "$STALE" | xargs -r kill 2>/dev/null || true
  fi
done
# Wait for all ports to be freed
sleep 2
for port in "${PORTS_TO_FREE[@]}"; do
  STALE="$(lsof -nP -iTCP:${port} -sTCP:LISTEN -t 2>/dev/null || true)"
  if [[ -n "$STALE" ]]; then
    warn "Port ${port} still in use — force-killing PID(s) $STALE..."
    echo "$STALE" | xargs -r kill -9 2>/dev/null || true
    sleep 1
  fi
done

# ── Start backend ─────────────────────────────────────────────────────────────
BACKEND_LOG="/tmp/llm-council-backend.log"
info "Starting backend (FastAPI on :8001)..."
uv run python -m backend.main > "$BACKEND_LOG" 2>&1 &
BACKEND_PID=$!
echo "$BACKEND_PID" >> "$PID_FILE"
info "  Backend PID: $BACKEND_PID (log: $BACKEND_LOG)"

# Give backend a moment to start
sleep 2
if ! kill -0 "$BACKEND_PID" 2>/dev/null; then
  error "Backend failed to start. Check $BACKEND_LOG"
fi

# ── Start frontend ────────────────────────────────────────────────────────────
FRONTEND_LOG="/tmp/llm-council-frontend.log"
cd frontend

if [[ "$MODE" == "dev" ]]; then
  FRONTEND_PORT=5173
  info "Starting frontend (Vite dev on :${FRONTEND_PORT})..."
  npm run dev -- --host 0.0.0.0 --port "$FRONTEND_PORT" > "$FRONTEND_LOG" 2>&1 &
  FRONTEND_PID=$!
  LOCAL_IP="$(hostname -I | awk '{print $1}')"
  ACCESS_URL="http://${LOCAL_IP}:${FRONTEND_PORT}"
else
  FRONTEND_PORT=4173
  info "Starting frontend (Vite preview on :${FRONTEND_PORT})..."
  npm run preview -- --host 0.0.0.0 --port "$FRONTEND_PORT" > "$FRONTEND_LOG" 2>&1 &
  FRONTEND_PID=$!
  LOCAL_IP="$(hostname -I | awk '{print $1}')"
  ACCESS_URL="http://${LOCAL_IP}:${FRONTEND_PORT}"
fi

echo "$FRONTEND_PID" >> "$PID_FILE"
cd ..
info "  Frontend PID: $FRONTEND_PID (log: $FRONTEND_LOG)"

# ── Wait for frontend to bind ─────────────────────────────────────────────────
sleep 2

# ── Done ──────────────────────────────────────────────────────────────────────
echo ""
echo -e "${GREEN}✅ LLM Council installed and running!${NC}"
echo ""
echo "  Mode:     $MODE"
echo "  API:      $API_MODE"
echo "  Backend:  http://127.0.0.1:8001"
echo "  Frontend: ${ACCESS_URL}"
echo ""
echo "  Quick query: /council-brief 'Your question here'"
echo "  Stop:        bash ${SKILL_DIR}/stop.sh"
echo "  Status:      bash ${SKILL_DIR}/status.sh"
echo "  Backend log:  $BACKEND_LOG"
echo "  Frontend log: $FRONTEND_LOG"
echo ""


path: status.sh
content: #!/usr/bin/env bash
# status.sh — Check if LLM Council services are running

SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PID_FILE="${SKILL_DIR}/pids"

GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'; NC='\033[0m'

echo "=== Council Brief / LLM Council Status ==="

# Check by PID file
if [[ -f "$PID_FILE" ]]; then
  echo ""
  echo "PIDs from last install:"
  while IFS= read -r pid; do
    if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
      CMD="$(ps -p "$pid" -o comm= 2>/dev/null || echo '?')"
      echo -e "  PID $pid: ${GREEN}RUNNING${NC} ($CMD)"
    else
      echo -e "  PID $pid: ${RED}NOT RUNNING${NC}"
    fi
  done < "$PID_FILE"
else
  echo -e "  ${YELLOW}No PID file — services have not been started or were stopped.${NC}"
fi

# Check ports directly
echo ""
echo "Port listeners:"
for port in 8001 4173 5173 5174; do
  if lsof -nP -iTCP:${port} -sTCP:LISTEN &>/dev/null; then
    PROC="$(lsof -nP -iTCP:${port} -sTCP:LISTEN 2>/dev/null | tail -1 | awk '{print $1}')"
    echo -e "  :${port} → ${GREEN}LISTENING${NC} ($PROC)"
  else
    echo -e "  :${port} → ${RED}nothing${NC}"
  fi
done

echo ""


path: stop.sh
content: #!/usr/bin/env bash
# stop.sh — Stop LLM Council background services

SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PID_FILE="${SKILL_DIR}/pids"

GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'

if [[ ! -f "$PID_FILE" ]]; then
  echo -e "${YELLOW}[council-brief]${NC} No PID file found — services may not be running."
  exit 0
fi

echo -e "${GREEN}[council-brief]${NC} Stopping LLM Council services..."

while IFS= read -r pid; do
  if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
    kill "$pid" && echo "  Killed PID $pid" || echo "  Failed to kill PID $pid"
  else
    echo "  PID $pid not running (already stopped)"
  fi
done < "$PID_FILE"

rm -f "$PID_FILE"
echo -e "${GREEN}[council-brief]${NC} Done."


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)