#!/usr/bin/env bash
# pqpw — one command to run a crew on any project.
#
#   pqpw new <name> "<Project Name>"   set up and open a new crew
#   pqpw open <name>                   reopen that crew's window
#   pqpw icon <name> "<Project Name>"  rebuild its desktop icon
#   pqpw auto on|off <name>            turn automatic agent-to-agent talk on/off
#   pqpw list                          crews on this computer
#
# Three panes, left to right, same colours everywhere: Coordinator blue,
# Architect teal, Builder rust.
set -eu
CREW="$HOME/.local/share/crew"; PAIRS="$CREW/pairs.json"; BIN="$HOME/bin"
mkdir -p "$CREW" "$BIN" "$HOME/.local/share/konsole"
[ -f "$PAIRS" ] || echo '{}' > "$PAIRS"
DOCS="${CREW_DOCS:-$HOME/agent-docs}"

pair_get() { python3 -c "import json,sys;print(json.load(open('$PAIRS')).get(sys.argv[1],{}).get(sys.argv[2],''))" "$1" "$2"; }

# The project name this crew registered.
crew_name() { local n; n="$(pair_get "$1" name)"; printf '%s' "${n:-$1}"; }

make_pane() {  # role colour Role command prompt
    # A pane's header says its role and nothing else. Konsole shows the same
    # string on the window title bar, copied from whichever pane has focus —
    # there is no separate window title, so the two cannot differ.
    # The profile keeps the project in its own name so open_window can pick it.
    local role="$1" colour="$2" Role="$3" cmd="$4" prompt="$5"
    local title="$Role" profile="$name $Role"
    local sid; sid="$(cat /proc/sys/kernel/random/uuid)"
    local file="$BIN/pqpw-$pair-$role"
    cat > "$file.new" <<PANE
#!/usr/bin/env bash
# $title for the $name crew. The three panes open and close as one window.
kpid="\$PPID"; [ "\$(ps -o comm= -p "\$kpid")" = konsole ] || kpid=""
close_crew() { trap - EXIT HUP TERM; [[ "\$kpid" =~ ^[0-9]+\$ ]] && kill "\$kpid" 2>/dev/null; }
trap close_crew EXIT HUP TERM
cd "$root"
printf '\033]11;$colour\007\033]0;$title\007'
# Bold has to survive. Claude Code works out what the terminal can do when it
# starts, and a pane opening beside two others can lose that race and draw the
# four labels as plain text for the rest of its life (23 Sep 2026).
export FORCE_COLOR=3
export COLORTERM=truecolor
# The pane title is set above and must stay put: a window title that keeps
# changing is what the crew-titles service has to fight (locked rules 11).
export CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1
# A pane keeps its own memory between openings. Claude Code saves no transcript
# while CLAUDE_CODE_CHILD_SESSION is set, and a pane inherits it from whatever
# launched it; without a transcript there is nothing to resume.
unset CLAUDE_CODE_CHILD_SESSION
CREW_SESSION="$sid"
if compgen -G "\$HOME/.claude/projects/*/\$CREW_SESSION.jsonl" > /dev/null; then
    CREW_RESUME=(--resume "\$CREW_SESSION")
else
    CREW_RESUME=(--session-id "\$CREW_SESSION")
    CREW_FRESH=1
fi
# The standing prompt is an opening instruction, not something to say again. A
# resumed pane carries none, or it re-reads every rule before doing any work.
CREW_PROMPT=()
[ -n "\$CREW_FRESH" ] && CREW_PROMPT=("$prompt")
$cmd "\${CREW_PROMPT[@]}"
PANE
    chmod +x "$file.new"; mv "$file.new" "$file"
    printf '[General]\nName=%s\nParent=FALLBACK/\nCommand=%s\nLocalTabTitleFormat=%s\nRemoteTabTitleFormat=%s\n' \
        "$profile" "$file" "$title" "$title" > "$HOME/.local/share/konsole/$profile.profile"
}

make_launcher() {  # pair name — the desktop icon. LOCKED: every crew gets one.
    # the owner clicks this instead of typing anything. It opens the crew in its OWN
    # Konsole process, so pressing Konsole in the task bar can never switch him
    # into another crew's window.
    local icons="$HOME/.local/share/icons/hicolor/scalable/apps"
    local apps="$HOME/.local/share/applications"
    local initial; initial="$(printf '%s' "$name" | cut -c1 | tr '[:lower:]' '[:upper:]')"
    mkdir -p "$icons" "$apps" "$HOME/Desktop"

    # Three agents over a desk, in the crew colours: teal, rust, blue.
    cat > "$icons/pqpw-crew-$pair.svg.new" <<SVG
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
  <rect x="1" y="1" width="62" height="62" rx="13" fill="#1a2a44"/>
  <rect x="10" y="24" width="44" height="7" rx="2" fill="#e6ecf5"/>
  <rect x="15" y="31" width="5" height="17" rx="1.5" fill="#e6ecf5"/>
  <rect x="44" y="31" width="5" height="17" rx="1.5" fill="#e6ecf5"/>
  <circle cx="20" cy="15" r="5.5" fill="#1f8f78" stroke="#e6ecf5" stroke-width="1.5"/>
  <circle cx="32" cy="15" r="5.5" fill="#c46a2c" stroke="#e6ecf5" stroke-width="1.5"/>
  <circle cx="44" cy="15" r="5.5" fill="#4d7fd1" stroke="#e6ecf5" stroke-width="1.5"/>
  <text x="32" y="53" font-family="sans-serif" font-size="17" font-weight="bold"
        text-anchor="middle" fill="#e6ecf5">$initial</text>
</svg>
SVG
    mv "$icons/pqpw-crew-$pair.svg.new" "$icons/pqpw-crew-$pair.svg"

    # The opener. Refuses to open a second window for the same crew.
    cat > "$BIN/pqpw-open-$pair.new" <<OPENER
#!/usr/bin/env bash
# Open the $name crew. One window, three panes, its own Konsole process.
# Match the Coordinator pane's own command, not its window title. A title can be
# renamed; that let a second crew window open on 15 Sep 2026 with three live
# agents in it. The command path never changes.
if pgrep -f "$BIN/pqpw-$pair-coordinator" >/dev/null 2>&1; then
    notify-send "$name crew" "Already open. Close that window first."
    exit 0
fi
exec "$BIN/pqpw" open "$pair"
OPENER
    chmod +x "$BIN/pqpw-open-$pair.new"; mv "$BIN/pqpw-open-$pair.new" "$BIN/pqpw-open-$pair"

    printf '[Desktop Entry]\nType=Application\nName=%s Crew\nComment=%s\nExec=%s\nIcon=pqpw-crew-%s\nTerminal=false\nCategories=Development;\n' \
        "$name" "Coordinator, Architect and Builder in one separate Konsole window" \
        "$BIN/pqpw-open-$pair" "$pair" > "$apps/pqpw-crew-$pair.desktop"
    # No per-crew icon on the desktop any more: Crews is the one icon and it
    # holds every crew. The launcher stays in the applications list for
    # anything that still wants to open one crew on its own.
    update-desktop-database "$apps" >/dev/null 2>&1 || true
    echo "launcher: $apps/pqpw-crew-$pair.desktop (open it from the Crews app)"
}

open_window() {
    # left to right: Coordinator, Architect, Builder
    konsole --separate --profile "$name Coordinator" >/dev/null 2>&1 < /dev/null &
    local kpid=$!; disown "$kpid" 2>/dev/null
    local svc="org.kde.konsole-$kpid"
    for _ in $(seq 1 40); do qdbus "$svc" /Windows/1 >/dev/null 2>&1 && break; sleep 0.25; done
    for prof in "$name Architect" "$name Builder"; do
        qdbus "$svc" /Windows/1 org.kde.konsole.Window.newSession "$prof" "$root" >/dev/null
        sleep 1
        local hier view splitter count
        hier="$(qdbus "$svc" /Windows/1 org.kde.konsole.Window.viewHierarchy)"
        splitter="$(printf '%s\n' "$hier" | sed -n 1p | grep -o '^([0-9]*)' | tr -d '()')"
        view="$(printf '%s\n' "$hier" | sed -n 2p | grep -o '\[[0-9]*\]' | tr -d '[]')"
        count="$(( $(printf '%s\n' "$hier" | sed -n 1p | tr -cd '|' | wc -c) + 1 ))"
        qdbus "$svc" /Windows/1 org.kde.konsole.Window.moveView "${view:-1}" "${splitter:-0}" "$count" >/dev/null
    done
    echo "$svc"
}

case "${1:-list}" in
new)
    pair="${2:?pqpw new <name> \"<Project Name>\"}"; name="${3:-$2}"
    root="${PQPW_ROOT:-$HOME}"
    chats="$DOCS/Chats/$(date +%Y-%m-%d) - $name crew"; mkdir -p "$chats/Reports"
    facts="$DOCS/$name - crew facts.md"
    [ -f "$facts" ] || cp "$DOCS/Agent crew - project facts template.md" "$facts"
    start="$DOCS/Agent crew - starting a new crew.md"

    # The standard crew. Override any of these to make a crew with different
    # models; leaving them alone gives the arrangement that is known to work.
    arch_model="${PQPW_ARCHITECT_MODEL:-gpt-5.6-sol}"
    arch_effort="${PQPW_ARCHITECT_EFFORT:-medium}"
    build_model="${PQPW_BUILDER_MODEL:-claude-sonnet-5}"
    build_effort="${PQPW_BUILDER_EFFORT:-low}"
    coord_model="${PQPW_COORDINATOR_MODEL:-claude-opus-5[1m]}"
    coord_effort="${PQPW_COORDINATOR_EFFORT:-medium}"

    common="Read '$DOCS/Agent crew - locked rules.md', '$DOCS/Agent crew - turn taking and timing.md', '$DOCS/Agent crew - the message contract.md' and '$facts'. Handoffs are files in '$chats' with START n or COMPLETED n on the first line. Project: $name."
    make_pane architect  '#0f2d26' "Architect" \
        "command codex -c model_auto_compact_token_limit=200000 -m $arch_model -c model_reasoning_effort=\"$arch_effort\" --dangerously-bypass-approvals-and-sandbox --sandbox danger-full-access" \
        "You are the ARCHITECT of the $name crew. $common Read the code, find causes, write one-change briefs, never edit files."
    make_pane builder    '#3a2414' "Builder" \
        "CLAUDE_CODE_AUTO_COMPACT_WINDOW=200000 command claude \"\${CREW_RESUME[@]}\" --model $build_model --effort $build_effort --dangerously-skip-permissions" \
        "You are the BUILDER of the $name crew. $common You are the only agent that edits files. Failing test first, implement the brief exactly, stage only your own changes, reply COMPLETED n. Never use the Complete/Found/Next/the owner shape for the Architect."
    make_pane coordinator '#1a2a44' "Coordinator" \
        "CLAUDE_CODE_AUTO_COMPACT_WINDOW=200000 command claude \"\${CREW_RESUME[@]}\" --model '$coord_model' --effort $coord_effort --dangerously-skip-permissions" \
        "You are the COORDINATOR of the $name crew. $common Also read ~/.claude/CLAUDE.md if you have one. You never edit project code and never commit. Judge whether fixes are right, set order, unblock stuck panes, answer the owner in his four blocks. Do not start crew-coordinator-wait or crew-stall-wait; they are retired and the relay watches for stalls itself. Answer agents with crew-say $pair architect|builder, look with crew-look $pair architect|builder 30. Turn agent-to-agent relay on with: pqpw auto on $pair."

    python3 - "$PAIRS" "$pair" "$chats" "$name" "$BIN" <<'PY'
import json, sys
path, pair, chats, name, binder = sys.argv[1:6]
data = json.load(open(path))
# Panes are found by the command each one runs. A title cannot identify a pane:
# all three of a crew's panes share one, and every crew's look alike.
data[pair] = {"chats": chats, "name": name,
              "ids": {r: f"{binder}/pqpw-{pair}-{r}"
                      for r in ("architect", "builder", "coordinator")}}
json.dump(data, open(path, "w"), indent=1)
PY
    make_launcher
    echo "crew '$pair' ready. chats: $chats"
    echo "facts page (fill it in): $facts"
    # The harness starts the panes itself, so it asks for no window.
    [ -n "${PQPW_NO_OPEN:-}" ] || open_window >/dev/null
    ;;
open)
    pair="${2:?pqpw open <name>}"; root="${PQPW_ROOT:-$HOME}"
    [ -n "$(pair_get "$pair" chats)" ] || { echo "no crew called $pair"; exit 1; }
    name="$(crew_name "$pair")"
    open_window >/dev/null ;;
icon)
    pair="${2:?pqpw icon <name> [\"<Project Name>\"]}"
    [ -n "$(pair_get "$pair" chats)" ] || { echo "no crew called $pair"; exit 1; }
    name="${3:-$(crew_name "$pair")}"
    make_launcher ;;
auto)
    state="${2:?pqpw auto on|off <name>}"; pair="${3:?pqpw auto on|off <name>}"
    unit="crew-relay-$pair"
    if [ "$state" = on ]; then
        systemctl --user stop "$unit" 2>/dev/null || true
        systemd-run --user --unit="$unit" --collect systemd-inhibit --what=sleep:idle \
            --who="$unit" --why="crew $pair" "$BIN/crew-relay" --pair="$pair" >/dev/null
        # The handoff watcher runs only while the relay does, so a paused crew
        # is never woken to write a handoff.
        systemctl --user stop "crew-handoff-$pair" 2>/dev/null || true
        systemd-run --user --unit="crew-handoff-$pair" --collect \
            "$BIN/crew-handoff" --pair="$pair" >/dev/null
        echo "auto discussion ON for $pair"
    else
        systemctl --user stop "$unit" 2>/dev/null || true
        systemctl --user stop "crew-handoff-$pair" 2>/dev/null || true
        echo "auto discussion OFF for $pair"
    fi ;;
go)
    pair="${2:?pqpw go <name> \"<what you want done>\"}"; shift 2; goal="$*"
    # One standing order, always the same shape: the Coordinator plans it,
    # asks the owner anything only he can decide, then drives the other two.
    "$BIN/crew-say" "$pair" coordinator "New job from the owner: $goal. Read the crew facts page first. Write a short plan as numbered sections, smallest visible win first, each with what the Builder will do and what the owner must watch. Ask the owner one question at a time only where a decision is genuinely his. Then brief the Architect and keep the crew moving; do not ask the owner to carry messages. Report back in his four blocks."
    echo "sent to the $pair coordinator" ;;
list)
    python3 -c "import json;d=json.load(open('$PAIRS'));print('\n'.join(f'{k}  ->  {v[\"chats\"]}' for k,v in d.items()) or 'no crews yet')"
    echo "no built-in crews; make one with: pqpw new <name> <folder>" ;;
*) sed -n '2,12p' "$0" ;;
esac
