#!/usr/bin/env bash # fork-fleet.sh — bootstrap a fresh fleet-management repo from this one. # # Mirrors the reusable tooling (scripts/, generic playbook templates, # conventions section of CLAUDE.md, .gitignore) into a new directory # and strips everything fleet-specific (servers/, stacks/, configs/, # fleet-named playbooks, runbooks, status). The output is an empty # but conventionally-organized workspace that's ready to populate # with a different fleet. # # Deliberately: # * does NOT create a remote / push — that's a manual step so the # name + namespace are explicit # * does NOT carry git history — the new fleet starts with a clean # "Initial commit" so the source repo's commit log doesn't bleed # across # * refuses to overwrite an existing directory # # Usage: # scripts/fork-fleet.sh [] # # Examples: # scripts/fork-fleet.sh ~/development/acme-prod-management # scripts/fork-fleet.sh /tmp/test-fork test-fleet # # Default fleet-name is the destination dir's basename. set -euo pipefail DEST="${1:?usage: $0 []}" FLEET_NAME="${2:-$(basename "$DEST" | sed 's/-management$//')}" SRC="$(cd "$(dirname "$0")/.." && pwd)" bold() { printf '\033[1m%s\033[0m\n' "$*"; } info() { printf ' %s\n' "$*"; } if [ -e "$DEST" ]; then echo "FATAL: $DEST already exists — refusing to overwrite" >&2 exit 1 fi bold "→ forking $SRC" info " destination : $DEST" info " fleet name : $FLEET_NAME" echo # ── 1. Mirror the reusable tooling ────────────────────────────────── # Excludes are explicit so adding a new fleet-specific dir to the # source doesn't accidentally leak into forks. mkdir -p "$DEST" rsync -a \ --exclude='.git/' \ --exclude='.claude/' \ --exclude='stacks-mirror/' \ --exclude='servers/*' \ --exclude='stacks/*' \ --exclude='configs/' \ --exclude='docs/orientation.md' \ --exclude='docs/runbooks/' \ --exclude='docs/pfi/' \ --exclude='MEMORY.md' \ --exclude='STATUS.md' \ --exclude='README.md' \ --exclude='CLAUDE.md' \ --exclude='playbooks/deploy-*' \ --exclude='playbooks/decouple-*' \ --exclude='scripts/upgrade-irv-ml1-cuda.sh' \ "$SRC/" "$DEST/" info "tooling copied" # ── 2. Replace gitkeeps in the now-empty fleet dirs ───────────────── mkdir -p "$DEST/servers" "$DEST/stacks" "$DEST/stacks-mirror" \ "$DEST/configs" "$DEST/docs/runbooks" touch "$DEST/servers/.gitkeep" "$DEST/stacks/.gitkeep" \ "$DEST/configs/.gitkeep" "$DEST/docs/runbooks/.gitkeep" # stacks-mirror is gitignored — no .gitkeep needed. # ── 3. Skeleton CLAUDE.md ─────────────────────────────────────────── cat > "$DEST/CLAUDE.md" <.yaml\` and run \`scripts/elway --playbook ...\` instead of chaining \`ssh -t host 'sudo …'\` commands — handles sudo once lazily, structured pass/change/fail reporting, idempotency via \`creates:\` / \`when:\` / \`changed_when:\`. Template: [\`playbooks/elway-smoke.yaml\`](playbooks/elway-smoke.yaml). **Task visibility via task-board.** If the Claude Code session has the \`task-board\` plugin enabled (installed from \`git@gitea.phasefinal.com:vh/task-board.git\`), a card tracks work in progress. Hooks flip the card on turn boundaries automatically; call \`task_start\` / \`task_update\` / \`task_wait\` / \`task_complete\` MCP tools to set the activity subheader and post meaningful log entries. When you launch a \`Bash\` tool with \`run_in_background: true\` (or any long-running shell / monitor / poll loop), call \`task_set_shells\` with one short description per active background shell — and call it again with the updated list (or \`[]\`) when one completes. The board flips a waiting card to **orange** while the list is non-empty so the user can tell at a glance the session is parked on background work, not stalled on them. Hooks have no way to enumerate the bg-task list externally, so this is on the assistant. ## Purpose - Inventory of servers and their state - Canonical copies of Docker Compose stacks deployed on those servers - Scripts for inspecting and managing the infrastructure - Conventions so all stacks look the same This is a **reference workspace** — the authoritative copies of compose files and configs live **on the servers** under \`/opt/docker/compose//\` and \`/opt/docker/conf//\`. This workspace mirrors them for version control, editing, and planning. ## Conventions (enforce for every new stack) - **Compose location on server:** \`/opt/docker/compose//compose.yaml\` - **Config mounts on server:** \`/opt/docker/conf//...\` - **Networks:** external \`traefik-net\`, aliased as \`tnet\` in compose \`\`\`yaml networks: tnet: name: traefik-net external: true \`\`\` - **GPU reservation:** prefer \`deploy.resources.reservations.devices\` with explicit \`device_ids\` for pinning \`\`\`yaml deploy: resources: reservations: devices: - driver: nvidia device_ids: ["1"] capabilities: [gpu] \`\`\` - **Tunables:** \`.env\` in the same directory as \`compose.yaml\` — keep the compose file constant, edit the \`.env\` - **Named volumes** for service state (pattern: \`_\`) - **Bind mounts** only for: model files, config files (\`/opt/docker/conf/...\`), docker socket where required - **Restart policy:** \`restart: unless-stopped\` for daemons - **Homepage labels** on user-facing services: \`\`\`yaml labels: - homepage.group=AI Systems - homepage.name= - homepage.icon=mdi- - homepage.description= - homepage.href=http://: \`\`\` - **Healthchecks** on services that expose HTTP ## Servers | Name | IP | Role | Details | |------|-----|------|---------| | _(none yet — \`scripts/add-host.sh \` to register)_ | | | | ## How to refresh a server's state \`\`\`bash scripts/refresh-server-info.sh # show help scripts/refresh-server-info.sh all # every host scripts/refresh-server-info.sh # one host scripts/refresh-server-info.sh --validate-only all \`\`\` To register a new server: \`\`\`bash scripts/add-host.sh scripts/refresh-server-info.sh \`\`\` ## Stack tree convention (canonical vs mirror) | tree | role | git | who writes | who reads | |---|---|---|---|---| | \`stacks//\` | **canonical / intent** | tracked | you / Claude | \`deploy-stack.sh\` | | \`stacks-mirror///\` | **snapshot / reality** | gitignored | \`sync-stacks.sh\` | drift inspection | \`\`\`bash \$EDITOR stacks//compose.yaml scripts/deploy-stack.sh # diffs vs live, prompts y/N scripts/sync-stacks.sh # drift check across all hosts diff -ru stacks// stacks-mirror/// \`\`\` ## Working rules - **Copies, not symlinks.** Files here reflect what's on the server at the time of the last sync. Editing here doesn't change the server until you deploy. - **Never commit secrets.** Use \`.env.example\` templates; real \`.env\` files live on the server (gitignored). - **Surgical edits.** When fixing one stack, don't touch unrelated ones. - **Sanity-check before deploying.** Run \`docker compose config\` (dry parse) before \`docker compose up -d\` on the server. EOF # ── 4. Skeleton README.md ─────────────────────────────────────────── cat > "$DEST/README.md" < # Pull a snapshot of its current state scripts/refresh-server-info.sh # Smoke-test the elway runner against it scripts/elway --playbook playbooks/elway-smoke.yaml \`\`\` ## Layout \`\`\` ${FLEET_NAME}-management/ ├── CLAUDE.md # workspace conventions + assistant guidance ├── README.md # this file ├── scripts/ # reusable tooling │ ├── elway # SSH-driven playbook runner (lazy sudo) │ ├── server_inspect.sh │ ├── refresh-server-info.sh │ ├── add-host.sh │ ├── deploy-stack.sh │ ├── sync-stacks.sh │ ├── add-digest-user.sh │ └── fork-fleet.sh # bootstrap another fleet from this one ├── playbooks/ │ └── elway-smoke.yaml # template playbook ├── servers// # one dir per registered host └── stacks// # one dir per canonical stack \`\`\` ## Onboarding the assistant This workspace is intended to be opened in a Claude Code session named after the fleet (e.g. \`${FLEET_NAME}-infra\`). The \`CLAUDE.md\` file is auto-loaded and gives the assistant fleet conventions and tool wiring. EOF # ── 5. Skeleton STATUS.md (optional rolling-status doc) ───────────── cat > "$DEST/STATUS.md" </dev/null; then git -c user.email=fork@local -c user.name=fork-fleet \ commit -q -m "Initial commit: forked from infra-management template Stripped fleet-specific content; kept the reusable tooling (scripts/, elway-smoke.yaml template, conventions section of CLAUDE.md, .gitignore). Fleet name: ${FLEET_NAME}." fi bold "" bold "✓ forked into $DEST" echo echo "next steps:" echo " cd $DEST" echo " # 1. Edit CLAUDE.md — fill in the Servers table + any placement rules" echo " # 2. Add your first host" echo " scripts/add-host.sh " echo " scripts/refresh-server-info.sh " echo " # 3. Wire up a remote when ready" echo " tea repo create vh/${FLEET_NAME}-management # or whatever namespace" echo " git remote add origin git@gitea.phasefinal.com:vh/${FLEET_NAME}-management.git" echo " git push -u origin main"