Utility Commands

commands no always-loaded rules -- loads on demand updated 2026-08-04

Utility commands and scripts: /cws-submit, /ccgm-sync (reverse-sync local config to CCGM), /user-test, statusline, agent-team launcher.

Tags

  • commands
  • utility
  • git
  • browser
  • testing
  • statusline
  • tmux

README

Utility Commands

Miscellaneous utility commands for common Claude Code workflows.

Commands

/cws-submit - Chrome Web Store Submission

Step-by-step walkthrough for submitting a Chrome extension to the Chrome Web Store. Uses walkthrough mode (one step at a time, waits for confirmation).

Handles: extension identification, prerequisites check, store assets preparation, Stripe promo code creation, production build, CWS dashboard walkthrough.

/cws-submit
/cws-submit gmail-darkly

Note: Reads docs/cws-submission-process.md in the repo for step-by-step instructions.

/ccgm-sync - Sync Local Config to CCGM

Delegates to a cheaper-model agent to reverse-sync local ~/.claude/ changes back to the CCGM repo. Shows a dry run first, then applies changes.

/ccgm-sync

Reads CCGM root from ~/.claude/.ccgm-manifest.json.

/user-test - Browser-Based User Testing

Simulates real user testing of a deployed web app using Chrome automation. Generates a problem-space doc and solution-space doc. Optionally auto-iterates to fix issues.

/user-test <url>
/user-test https://myapp.com --flows "login, search, checkout" --persona "new user"
/user-test https://myapp.com --iterate 3

Produces docs/user-test-problems.md and docs/user-test-solutions.md in the project directory.

Manual Installation

mkdir -p ~/.claude/commands
cp commands/cws-submit.md ~/.claude/commands/cws-submit.md
cp commands/ccgm-sync.md ~/.claude/commands/ccgm-sync.md
cp commands/user-test.md ~/.claude/commands/user-test.md

# Statusline
cp statusline-command.sh ~/.claude/statusline-command.sh
chmod +x ~/.claude/statusline-command.sh

# Scripts
mkdir -p ~/.claude/scripts
cp scripts/ccgm-sync.sh ~/.claude/scripts/ccgm-sync.sh
cp scripts/agent-team ~/.claude/scripts/agent-team
chmod +x ~/.claude/scripts/ccgm-sync.sh
chmod +x ~/.claude/scripts/agent-team

Will install

Path Action Target Type
commands/cws-submit.md commands/cws-submit.md command
commands/ccgm-sync.md commands/ccgm-sync.md command
commands/user-test.md commands/user-test.md command
statusline-command.sh statusline-command.sh script
scripts/ccgm-sync.sh scripts/ccgm-sync.sh script
scripts/agent-team scripts/agent-team script

Dependencies

No dependencies.

Required by

No other module depends on this one.

Included in presets

Install this module

Agent prompt

Recommended for agent users -- hands the whole install off to your assistant.

Fetch https://cd23a9be.ccgm-site.pages.dev/modules/commands-utility.md and install this module into my Claude Code setup.

Native plugin marketplace

One command via the native plugin marketplace -- additive, does not merge settings.json.

claude plugin install commands-utility@ccgm

The marketplace path is additive, not a replacement: it installs commands, agents, and skills as native plugin components, but it does not perform the bash installer's deep settings.json merge, and it does not write the always-loaded global CLAUDE.md context. Rules are only injected via an opt-in SessionStart hook rather than being auto-loaded. Use the bash installer when those pieces matter to you.

Manual, per file

Full control -- copy exactly the files you want from the sections below.

Files

Files

command (3)

commands/cws-submit.md

Walk me through submitting a Chrome extension to the Chrome Web Store.

## Context

Read `docs/cws-submission-process.md` for the full step-by-step process. Use it as the source of truth for all steps.

## What to do

1. **Identify the extension** — determine which extension package is being submitted (gmail-darkly, sheets-darkly, docs-darkly, or darkly-suite)

2. **Check prerequisites** — verify landing page, payment API, Stripe config, and E2E testing are complete

3. **Prepare store assets** — check if `packages/{extension}/store-assets/` exists with all required files. If any are missing, create them using Gmail Darkly's store assets as a template (`packages/gmail-darkly/store-assets/`)

4. **Create reviewer promo code** — use Stripe CLI to create a 100% off coupon and promo code. Also create a second one for the user to test with.

5. **Build production zip** — `pnpm --filter {extension} build` then zip the dist directory

6. **Walk through dashboard** — guide the user through each CWS dashboard tab one step at a time using the walkthrough pattern (one step, wait for confirmation, then next)

## Important

- Use `/walkthrough` behavior: one step at a time, wait for user confirmation
- Put all copy-paste text in `.txt` files so the user can copy without formatting
- Max 500 characters for test instructions
- Test instructions MUST include the test card number (4242 4242 4242 4242) — Stripe requires a card even with 100% off promo codes on subscriptions
- Test instructions can be edited after submission without resubmitting
- Always recommend **unchecking** auto-publish (stage for manual publish)
- Remind about live Stripe promo code recreation after approval
- Update the submissions log in `docs/cws-submission-process.md` after submission
- CRITICAL: Test the extension yourself (or ask the user to describe the actual UI) before writing test instructions. Do NOT assume UI behavior — describe exactly what happens.

$ARGUMENTS

commands/ccgm-sync.md

---
description: Sync local Claude Code config changes back to the CCGM repo
allowed-tools: Agent
---

# Sync Local Config (/ccgm-sync)

Use the Agent tool to execute this entire workflow on a cheaper model:

- **model**: sonnet
- **description**: ccgm-sync

Pass the agent all workflow instructions below.

After the agent completes, relay its report to the user exactly as received.

---

## Workflow Instructions

Reverse-sync local `~/.claude/` changes back to the **CCGM repo** - the source of truth for all global Claude Code configs.

**CCGM root**: Read from `~/.claude/.ccgm-manifest.json` -> `ccgmRoot` field.

### 0. Broken Symlink Check

Check for broken symlinks in `~/.claude/`. These happen when modules are renamed or removed while the install is in link mode.

```bash
find ~/.claude/commands ~/.claude/rules ~/.claude/hooks ~/.claude/bin ~/.claude/skills -type l ! -exec test -e {} \; -print 2>/dev/null
```

If any broken symlinks are found:
1. List each one with its dead target (`ls -la` on each)
2. Report them to the user
3. For each broken symlink, check if the target file moved to a new location in the CCGM repo (e.g., module was renamed). If found, fix the symlink to point to the new location. If not found, remove the broken symlink and note it.

Continue with the rest of the sync after resolving broken symlinks.

### 1. CCGM Sync - Run Dry First (Preview Changes)

```bash
bash ~/.claude/scripts/ccgm-sync.sh --dry
```

Show what drifted files and unmanaged files were found.

### 2. CCGM Sync - Apply

If there are drifted files, run:

```bash
bash ~/.claude/scripts/ccgm-sync.sh
```

This copies local changes back to CCGM module directories, commits, and pushes.

### 3. Report Results

Tell the user:
- Which broken symlinks were found and fixed (if any)
- Which CCGM module files were updated (if any)
- Which files are unmanaged (not tracked by any CCGM module)
- Whether changes were committed and pushed
- The current sync status

### 4. Run /docupdate (if CCGM files changed)

If any files were synced back to CCGM (step 2 made changes), run `/docupdate` to catch any documentation drift introduced by those changes.

This ensures module counts, command references, and feature descriptions in README, docs/, and module READMEs stay accurate after every sync.

commands/user-test.md

# /user-test - Browser-Based User Testing Simulation

Simulate real user testing of a deployed web app using Chrome automation. Generates a problem-space doc and solution-space doc, then optionally auto-iterates.

## Usage
```
/user-test <url> [--flows "login, search, checkout"] [--persona "new user"] [--iterate N]
```

## Workflow

### Phase 1: Setup

1. Parse the target URL and optional flow descriptions
2. If no flows specified, read the app's README or CLAUDE.md to understand key user flows
3. Get browser context and navigate to the target URL

### Phase 2: Explore & Test

For each user flow, simulate a real user:

1. **Navigate** to the starting point
2. **Read the page** - what does a user see? Is the intent clear?
3. **Interact** - click buttons, fill forms, navigate menus
4. **Check for errors** - console errors, network failures, broken UI
5. **Assess UX** - is it intuitive? confusing? slow? ugly?
6. **Screenshot** key states for evidence

Record every issue found with:
- **What happened** (factual observation)
- **Expected behavior** (what a user would expect)
- **Severity** (blocker / major / minor / cosmetic)
- **Screenshot** or console evidence

### Phase 3: Problem Space Document

Write `docs/user-test-problems.md` in the project directory:

```markdown
# User Test Report - [App Name]
**Date**: YYYY-MM-DD
**URL**: [tested URL]
**Persona**: [who we simulated]

## Critical Issues
- [blocker-level problems]

## UX Issues  
- [confusing flows, unclear UI, accessibility gaps]

## Visual Issues
- [layout bugs, inconsistencies, responsive problems]

## Performance Issues
- [slow loads, janky interactions]

## Console/Network Errors
- [JS errors, failed API calls, 4xx/5xx responses]
```

### Phase 4: Solution Space Document

Write `docs/user-test-solutions.md` in the project directory:

```markdown
# Solution Proposals - [App Name]
**Based on**: user-test-problems.md

## Priority Fixes (address these first)
For each critical/major issue:
- **Problem**: [reference]
- **Root cause**: [what's actually wrong in the code]
- **Proposed fix**: [specific code change]
- **Files**: [which files to modify]

## UX Improvements
- [design changes, flow improvements]

## Quick Wins
- [cosmetic fixes, easy improvements]
```

### Phase 5: Auto-Iterate (if --iterate N)

If `--iterate` flag is set:
1. Create GitHub issues from the solution doc (one per fix category)
2. Fix the top N issues automatically
3. Re-deploy and re-test to verify fixes
4. Update both docs with results

## Guidelines

- Test as a real user would - don't rely on knowing the codebase
- Check mobile viewport too (resize browser)
- Test error states - submit empty forms, use invalid data
- Check loading states - are there spinners? skeleton screens?
- Verify auth flows work end-to-end
- Look for accessibility basics (contrast, focus indicators, alt text)
- Time key interactions - anything over 2 seconds is a problem

## Output

After completing the test:
1. Print a summary of findings (counts by severity)
2. Point the user to the two docs
3. Ask if they want to auto-fix the top issues
script (3)

statusline-command.sh

#!/usr/bin/env bash
# Claude Code status line script
# Displays: model | directory branch | context usage | 5h & 7d rate limits

input=$(cat)

# --- Model: render as "{family}-{version}" with a tier that drives color/emoji.
# Family and version are parsed from display_name generically, so new VERSIONS
# of known families need no edits here; a new FAMILY needs one case branch.
# Examples:
#   "Fable 5" -> 🧠 F-5   "Opus 4.8 (1M context)" -> 🧠 O-4.8
#   "Sonnet 4.6" -> 🐢 S-4.6   "Haiku 4.5" -> ⚠️ H-4.5
model_raw=$(echo "$input" | jq -r '.model.display_name // ""')

# First version token in the name, e.g. "4.8" (empty when the name has none).
model_ver=$(printf '%s' "$model_raw" | grep -oE '[0-9]+(\.[0-9]+)?' | head -n1)

case "$model_raw" in
  *Fable*)
    # Fable is the tier above Opus — always flagship.
    model_abbr="F${model_ver:+-$model_ver}"
    model_tier="flagship"
    ;;
  *Opus*)
    model_abbr="O${model_ver:+-$model_ver}"
    # Opus is flagship (🧠) at 4.6 or newer, plain below. The check is numeric
    # and monotonic, so future versions (4.9, 5.0, …) are flagship automatically.
    ver_major=${model_ver%%.*}; ver_minor=${model_ver#*.}
    [ "$ver_minor" = "$model_ver" ] && ver_minor=0
    if [ "${ver_major:-0}" -gt 4 ] || { [ "${ver_major:-0}" -eq 4 ] && [ "${ver_minor:-0}" -ge 6 ]; }; then
      model_tier="flagship"
    else
      model_tier="opus-other"
    fi
    ;;
  *Sonnet*) model_abbr="S${model_ver:+-$model_ver}"; model_tier="sonnet" ;;
  *Haiku*)  model_abbr="H${model_ver:+-$model_ver}"; model_tier="haiku" ;;
  "")       model_abbr=""; model_tier="" ;;
  *)        model_abbr=$(echo "$model_raw" | sed 's/Claude //;s/ .*//'); model_tier="unknown" ;;
esac

# --- Directory: immediate dir name only
cwd=$(echo "$input" | jq -r '.cwd // ""')
cwd_display="${cwd##*/}"

# --- Effort level (stdin > env > project local > project > user settings)
# Claude Code (>=2.1.x) passes the live session effort level via stdin under
# `.effort.level`. This reflects in-session overrides like `/effort max` that
# never touch settings.json, so it must win over the persisted sources.
read_effort_from() {
  [ -f "$1" ] || return 1
  jq -r '.effortLevel // empty' "$1" 2>/dev/null
}
effort_raw=$(echo "$input" | jq -r '.effort.level // empty')
[ -z "$effort_raw" ] && effort_raw="${CLAUDE_CODE_EFFORT_LEVEL:-}"
if [ -z "$effort_raw" ] && [ -n "$cwd" ]; then
  effort_raw=$(read_effort_from "$cwd/.claude/settings.local.json")
  [ -z "$effort_raw" ] && effort_raw=$(read_effort_from "$cwd/.claude/settings.json")
fi
[ -z "$effort_raw" ] && effort_raw=$(read_effort_from "$HOME/.claude/settings.json")

# Ultracode is a session mode (xhigh effort + standing dynamic-workflow
# orchestration), tracked as a SEPARATE boolean from effort. Claude Code (2.1.x)
# resolves .effort.level to "xhigh" when it's on and does NOT put the flag in the
# statusline payload, so a session-only toggle is invisible here — we can only
# see it via the `ultracode` settings key. Check every channel that could declare
# it (mirroring the effort cascade) so this also auto-works if a future CC adds
# `.ultracode` / effort=="ultracode" to stdin.
read_ultracode_from() {
  [ -f "$1" ] && jq -e '.ultracode == true' "$1" >/dev/null 2>&1
}
ultracode=""
if   [ "$(echo "$input" | jq -r '.ultracode // empty')" = "true" ]; then ultracode=1
elif [ "$effort_raw" = "ultracode" ] || [ "${CLAUDE_CODE_EFFORT_LEVEL:-}" = "ultracode" ]; then ultracode=1
elif [ -n "$cwd" ] && read_ultracode_from "$cwd/.claude/settings.local.json"; then ultracode=1
elif [ -n "$cwd" ] && read_ultracode_from "$cwd/.claude/settings.json"; then ultracode=1
elif read_ultracode_from "$HOME/.claude/settings.json"; then ultracode=1
fi

case "$effort_raw" in
  low)    effort_abbr="L" ;;
  medium) effort_abbr="M" ;;
  high)   effort_abbr="H" ;;
  xhigh)  effort_abbr="XH" ;;
  max)    effort_abbr="Max" ;;
  *)      effort_abbr="" ;;
esac
# Ultracode always runs at xhigh effort, so show the accurate level (XH); the
# render bookends it with sparkles (✨XH✨) to flag the mode without hiding it.
[ -n "$ultracode" ] && effort_abbr="XH"

# --- Git branch (skip optional locks to avoid hangs in multi-clone repos)
git_branch=""
if [ -d "$cwd/.git" ] || git -C "$cwd" rev-parse --git-dir >/dev/null 2>&1; then
  git_branch=$(git -C "$cwd" -c core.fsmonitor=false symbolic-ref --short HEAD 2>/dev/null \
    || git -C "$cwd" -c core.fsmonitor=false rev-parse --short HEAD 2>/dev/null)
fi

# --- Context used (inverted from remaining)
remaining=$(echo "$input" | jq -r '.context_window.remaining_percentage // empty')

# --- Rate limits (5h session + 7-day)
five_hour=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
five_hour_resets=$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // empty')
weekly=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty')
weekly_resets=$(echo "$input" | jq -r '.rate_limits.seven_day.resets_at // empty')

# --- ANSI color codes
RESET='\033[0m'
CYAN='\033[36m'
YELLOW='\033[33m'
GREEN='\033[32m'
RED='\033[31m'
DIM='\033[2m'
BLUE='\033[34m'
ORANGE='\033[38;5;208m'

# Compact usage bar: 5 chars wide
make_bar() {
  local pct=$1 color=$2
  local bar_len=5
  local filled=$(( (pct * bar_len + 50) / 100 ))
  [ "$filled" -gt "$bar_len" ] && filled=$bar_len
  [ "$filled" -lt 0 ] && filled=0
  local empty=$(( bar_len - filled ))
  local bar=""
  for ((i=0; i<filled; i++)); do bar="${bar}█"; done
  local empty_part=""
  for ((i=0; i<empty; i++)); do empty_part="${empty_part}░"; done
  printf "${color}%s${DIM}%s${RESET}" "$bar" "$empty_part"
}

SEP=$(printf " ${DIM}|${RESET} ")
sections=()

# Model with tier indicators + optional effort suffix
if [ -n "$model_abbr" ]; then
  effort_suffix=""
  if [ -n "$effort_abbr" ]; then
    case "$effort_abbr" in
      Max) effort_color="$RED" ;;
      XH)  effort_color="$ORANGE" ;;
      H)   effort_color="$YELLOW" ;;
      M)   effort_color="$GREEN" ;;
      *)   effort_color="$DIM" ;;
    esac
    if [ -n "$ultracode" ]; then
      # Ultracode: keep the accurate effort color, bookend with sparkles (✨XH✨).
      effort_suffix="$(printf " ✨${effort_color}%s${RESET}✨" "$effort_abbr")"
    else
      effort_suffix="$(printf " ${effort_color}%s${RESET}" "$effort_abbr")"
    fi
  fi
  case "$model_tier" in
    flagship)
      sections+=("$(printf "${BLUE}🧠 %s${RESET}%s" "$model_abbr" "$effort_suffix")")
      ;;
    sonnet)
      sections+=("$(printf "${ORANGE}🐢 %s${RESET}%s" "$model_abbr" "$effort_suffix")")
      ;;
    haiku)
      sections+=("$(printf "${RED}⚠️ %s${RESET}%s" "$model_abbr" "$effort_suffix")")
      ;;
    *)
      sections+=("$(printf "%s%s" "$model_abbr" "$effort_suffix")")
      ;;
  esac
fi

# Dir + git branch (combined)
dir_part="$(printf "${CYAN}%s${RESET}" "$cwd_display")"
if [ -n "$git_branch" ]; then
  dir_part="${dir_part} $(printf "${YELLOW}%s${RESET}" "$git_branch")"
fi
sections+=("$dir_part")

# Context used (100 - remaining)
if [ -n "$remaining" ]; then
  remaining_int=$(printf '%.0f' "$remaining")
  used_int=$((100 - remaining_int))
  if [ "$used_int" -lt 60 ]; then
    ctx_color="$GREEN"
  elif [ "$used_int" -lt 85 ]; then
    ctx_color="$YELLOW"
  else
    ctx_color="$RED"
  fi
  sections+=("$(printf "${ctx_color}ctx:${used_int}%%${RESET}")")
fi

# 5-hour rate limit with bar and reset countdown
if [ -n "$five_hour" ]; then
  five_int=$(printf '%.0f' "$five_hour")
  if [ "$five_int" -lt 60 ]; then
    rl_color="$GREEN"
  elif [ "$five_int" -lt 85 ]; then
    rl_color="$YELLOW"
  else
    rl_color="$RED"
  fi
  five_part="$(printf "${rl_color}5h:${five_int}%%${RESET} ")$(make_bar "$five_int" "$rl_color")"
  if [ -n "$five_hour_resets" ]; then
    now=$(date +%s)
    diff=$(( five_hour_resets - now ))
    if [ "$diff" -gt 0 ]; then
      hours=$(( diff / 3600 ))
      mins=$(( (diff % 3600) / 60 ))
      if [ "$hours" -gt 0 ]; then
        five_part="${five_part} $(printf "${DIM}${hours}h${mins}m${RESET}")"
      else
        five_part="${five_part} $(printf "${DIM}${mins}m${RESET}")"
      fi
    fi
  fi
  sections+=("$five_part")
fi

# 7-day rate limit with bar and reset countdown
if [ -n "$weekly" ]; then
  weekly_int=$(printf '%.0f' "$weekly")
  if [ "$weekly_int" -lt 60 ]; then
    wk_color="$GREEN"
  elif [ "$weekly_int" -lt 85 ]; then
    wk_color="$YELLOW"
  else
    wk_color="$RED"
  fi
  wk_part="$(printf "${wk_color}7d:${weekly_int}%%${RESET} ")$(make_bar "$weekly_int" "$wk_color")"
  if [ -n "$weekly_resets" ]; then
    now=$(date +%s)
    diff=$(( weekly_resets - now ))
    if [ "$diff" -gt 0 ]; then
      days=$(( diff / 86400 ))
      hours=$(( (diff % 86400) / 3600 ))
      if [ "$days" -gt 0 ]; then
        wk_part="${wk_part} $(printf "${DIM}${days}d${hours}h${RESET}")"
      else
        mins=$(( (diff % 3600) / 60 ))
        wk_part="${wk_part} $(printf "${DIM}${hours}h${mins}m${RESET}")"
      fi
    fi
  fi
  sections+=("$wk_part")
fi

# Join sections with pipe separator
output=""
for i in "${!sections[@]}"; do
  if [ "$i" -gt 0 ]; then
    output="${output}${SEP}"
  fi
  output="${output}${sections[$i]}"
done

printf "%s" "$output"

scripts/ccgm-sync.sh

#!/usr/bin/env bash
# ccgm-sync.sh: Reverse-sync local ~/.claude changes back to CCGM module sources
#
# Reads .ccgm-manifest.json to find managed files, compares local vs module source,
# copies drifted files back, and reports unmanaged files.
#
# Usage:
#   ccgm-sync.sh [--dry]    Preview what would change
#   ccgm-sync.sh             Sync, commit, push, and merge

set -euo pipefail

MANIFEST="${HOME}/.claude/.ccgm-manifest.json"
DRY_RUN=false

if [[ "${1:-}" == "--dry" ]]; then
  DRY_RUN=true
fi

# --- Validate prerequisites ---
if [ ! -f "$MANIFEST" ]; then
  echo "ERROR: CCGM manifest not found at $MANIFEST"
  echo "Is CCGM installed? Run start.sh first."
  exit 1
fi

if ! command -v jq &>/dev/null; then
  echo "ERROR: jq is required but not found"
  exit 1
fi

CCGM_ROOT=$(jq -r '.ccgmRoot' "$MANIFEST")
LINK_MODE=$(jq -r '.linkMode // false' "$MANIFEST")

if [ ! -d "$CCGM_ROOT" ]; then
  echo "ERROR: CCGM root not found: $CCGM_ROOT"
  exit 1
fi

if [ "$LINK_MODE" = "true" ]; then
  echo "CCGM is in link mode - files are already symlinked to module sources."
  echo "No reverse-sync needed. Changes are automatically in the repo."
  echo ""
  echo "Just commit and push:"
  echo "  cd $CCGM_ROOT && git add -A && git commit && git push"
  exit 0
fi

# --- Collect drifted files ---
echo "Checking for local changes to CCGM-managed files..."
echo ""

DRIFTED=()
DRIFTED_PAIRS=()  # "module|src|target" triples

MODULES=$(jq -r '.modules[]?' "$MANIFEST")

while IFS= read -r mod; do
  [ -z "$mod" ] && continue
  MODULE_JSON="${CCGM_ROOT}/modules/${mod}/module.json"
  [ ! -f "$MODULE_JSON" ] && continue

  # Read file mappings from module.json
  while IFS= read -r file_entry; do
    src=$(echo "$file_entry" | jq -r '.src')
    target=$(echo "$file_entry" | jq -r '.target')
    template=$(echo "$file_entry" | jq -r '.template')
    merge=$(echo "$file_entry" | jq -r '.merge // false')

    full_src="${CCGM_ROOT}/modules/${mod}/${src}"
    full_target="${HOME}/.claude/${target}"

    # Skip templates and merge files (they're generated, not 1:1)
    if [ "$template" = "true" ] || [ "$merge" = "true" ]; then
      continue
    fi

    # Skip if either file doesn't exist
    [ ! -f "$full_src" ] && continue
    [ ! -f "$full_target" ] && continue

    # Compare
    if ! diff -q "$full_src" "$full_target" &>/dev/null; then
      DRIFTED+=("$target")
      DRIFTED_PAIRS+=("${mod}|${src}|${target}")

      if $DRY_RUN; then
        echo "CHANGED: $target (module: $mod)"
        diff --color=auto "$full_src" "$full_target" 2>/dev/null | head -30 || true
        echo ""
      fi
    fi
  done < <(jq -r '.files | to_entries[] | {src: .key, target: .value.target, template: (.value.template // false), merge: (.value.merge // false)} | @json' "$MODULE_JSON" 2>/dev/null)
done <<< "$MODULES"

# --- Check for unmanaged files ---
echo "Checking for new files not tracked by CCGM..."

MANAGED_TARGETS=()
while IFS= read -r mod; do
  [ -z "$mod" ] && continue
  MODULE_JSON="${CCGM_ROOT}/modules/${mod}/module.json"
  [ ! -f "$MODULE_JSON" ] && continue
  while IFS= read -r t; do
    MANAGED_TARGETS+=("$t")
  done < <(jq -r '.files | to_entries[] | .value.target' "$MODULE_JSON" 2>/dev/null)
done <<< "$MODULES"

UNMANAGED=()
# Check commands/
for f in "${HOME}/.claude/commands/"*.md; do
  [ ! -f "$f" ] && continue
  rel="commands/$(basename "$f")"
  found=false
  for mt in "${MANAGED_TARGETS[@]}"; do
    if [ "$mt" = "$rel" ]; then found=true; break; fi
  done
  if ! $found; then
    UNMANAGED+=("$rel")
  fi
done

# Check rules/
for f in "${HOME}/.claude/rules/"*.md; do
  [ ! -f "$f" ] && continue
  rel="rules/$(basename "$f")"
  found=false
  for mt in "${MANAGED_TARGETS[@]}"; do
    if [ "$mt" = "$rel" ]; then found=true; break; fi
  done
  if ! $found; then
    UNMANAGED+=("$rel")
  fi
done

# Check hooks/
for f in "${HOME}/.claude/hooks/"*.py; do
  [ ! -f "$f" ] && continue
  rel="hooks/$(basename "$f")"
  found=false
  for mt in "${MANAGED_TARGETS[@]}"; do
    if [ "$mt" = "$rel" ]; then found=true; break; fi
  done
  if ! $found; then
    UNMANAGED+=("$rel")
  fi
done

# --- Report ---
echo "=============================="
echo "  CCGM Sync Summary"
echo "=============================="
echo ""

if [ ${#DRIFTED[@]} -eq 0 ]; then
  echo "No drifted files - CCGM modules are in sync with local."
else
  echo "Drifted files (${#DRIFTED[@]}):"
  for f in "${DRIFTED[@]}"; do
    echo "  * $f"
  done
fi
echo ""

if [ ${#UNMANAGED[@]} -gt 0 ]; then
  echo "Unmanaged files (${#UNMANAGED[@]}) - not tracked by any CCGM module:"
  for f in "${UNMANAGED[@]}"; do
    echo "  ? $f"
  done
  echo ""
  echo "To add these to CCGM, create a new module or add to an existing one."
fi

if $DRY_RUN; then
  echo ""
  echo "Dry run complete. Run without --dry to apply changes."
  exit 0
fi

# --- Apply: copy local -> module source ---
if [ ${#DRIFTED[@]} -eq 0 ]; then
  echo ""
  echo "Nothing to sync."
  exit 0
fi

echo ""
echo "Syncing ${#DRIFTED[@]} file(s) back to CCGM modules..."

for pair in "${DRIFTED_PAIRS[@]}"; do
  IFS='|' read -r mod src target <<< "$pair"
  full_src="${CCGM_ROOT}/modules/${mod}/${src}"
  full_target="${HOME}/.claude/${target}"
  cp "$full_target" "$full_src"
  echo "  Copied: $target -> modules/${mod}/${src}"
done

# --- Commit, push, merge ---
echo ""
echo "Committing changes to CCGM repo..."

cd "$CCGM_ROOT"

# Check current branch
CURRENT_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "detached")

if [ "$CURRENT_BRANCH" = "main" ]; then
  # Direct commit on main (simple sync)
  git add -A
  if git diff --cached --quiet; then
    echo "No changes to commit (files already match)."
    exit 0
  fi
  git commit -m "sync: reverse-sync local config changes ($(date +%Y-%m-%d))"
  echo "Pushing to remote..."
  git push origin main
  echo ""
  echo "Done! CCGM repo updated on main."
else
  # On a branch - commit and push branch
  git add -A
  if git diff --cached --quiet; then
    echo "No changes to commit."
    exit 0
  fi
  git commit -m "sync: reverse-sync local config changes ($(date +%Y-%m-%d))"
  echo "Pushing branch $CURRENT_BRANCH..."
  git push -u origin "$CURRENT_BRANCH"
  echo ""
  echo "Branch pushed. Create a PR to merge into main:"
  echo "  gh pr create --title 'sync: local config changes' --body 'Reverse-sync from local ~/.claude'"
fi

echo ""
echo "Sync complete."

scripts/agent-team

#!/usr/bin/env bash
# agent-team: Launch N Claude Code agents in tmux panes for a workspace or repo
#
# Usage:
#   agent-team <repo-name> [clone-count]
#   agent-team <repo-name> --workspace <workspace-num> [clone-count]
#
# Examples:
#   agent-team darkly-suite 4          # Launch 4 agents for darkly-suite-repos clones
#   agent-team provendoro --workspace 1 3  # Launch 3 agents in provendoro workspace 1
#   agent-team openslide-ai            # Launch all available clones (auto-detect)
#
# The script creates a tmux session named after the repo, with one pane per agent.
# Each pane is titled with the agent identity and starts Claude Code in that clone dir.

set -euo pipefail

REPO=""
WORKSPACE=""
CLONE_COUNT=""
BASE_DIR="$HOME/code"

usage() {
  echo "Usage: agent-team <repo-name> [--workspace N] [clone-count]"
  echo ""
  echo "  repo-name       Name of the repo (e.g., darkly-suite, provendoro)"
  echo "  --workspace N   Use workspace model (workspace number)"
  echo "  clone-count     Number of clones to launch (default: auto-detect)"
  echo ""
  echo "Examples:"
  echo "  agent-team darkly-suite 4"
  echo "  agent-team provendoro --workspace 1 3"
  exit 1
}

# Parse arguments
while [[ $# -gt 0 ]]; do
  case $1 in
    --workspace|-w)
      WORKSPACE="$2"
      shift 2
      ;;
    --help|-h)
      usage
      ;;
    *)
      if [ -z "$REPO" ]; then
        REPO="$1"
      elif [ -z "$CLONE_COUNT" ]; then
        CLONE_COUNT="$1"
      fi
      shift
      ;;
  esac
done

[ -z "$REPO" ] && usage

# Determine clone directories
CLONE_DIRS=()

if [ -n "$WORKSPACE" ]; then
  # Workspace model: {repo}-workspaces/{repo}-wN/{repo}-wN-cM/
  WS_DIR="$BASE_DIR/${REPO}-workspaces/${REPO}-w${WORKSPACE}"
  if [ ! -d "$WS_DIR" ]; then
    echo "Error: Workspace directory not found: $WS_DIR"
    exit 1
  fi
  for d in "$WS_DIR"/${REPO}-w${WORKSPACE}-c*/; do
    [ -d "$d" ] && CLONE_DIRS+=("$d")
  done
else
  # Flat clone model: {repo}-repos/{repo}-N/
  REPOS_DIR="$BASE_DIR/${REPO}-repos"
  if [ ! -d "$REPOS_DIR" ]; then
    echo "Error: Repos directory not found: $REPOS_DIR"
    exit 1
  fi
  for d in "$REPOS_DIR"/${REPO}-*/; do
    [ -d "$d" ] && CLONE_DIRS+=("$d")
  done
fi

if [ ${#CLONE_DIRS[@]} -eq 0 ]; then
  echo "Error: No clone directories found"
  exit 1
fi

# Apply clone count limit if specified
if [ -n "$CLONE_COUNT" ] && [ "$CLONE_COUNT" -lt ${#CLONE_DIRS[@]} ]; then
  CLONE_DIRS=("${CLONE_DIRS[@]:0:$CLONE_COUNT}")
fi

NUM_CLONES=${#CLONE_DIRS[@]}

# Session name
if [ -n "$WORKSPACE" ]; then
  SESSION="${REPO}-w${WORKSPACE}"
else
  SESSION="${REPO}"
fi

echo "Launching $NUM_CLONES agents for $SESSION..."

# Kill existing session if present
tmux kill-session -t "$SESSION" 2>/dev/null || true

# Create session with first clone
FIRST_DIR="${CLONE_DIRS[0]}"
FIRST_NAME=$(basename "$FIRST_DIR")
tmux new-session -d -s "$SESSION" -c "$FIRST_DIR" -n "agents"

# Set pane title for first pane
tmux select-pane -t "$SESSION:1.1" -T "$FIRST_NAME"

# Create remaining panes
for ((i=1; i<NUM_CLONES; i++)); do
  CLONE_DIR="${CLONE_DIRS[$i]}"
  CLONE_NAME=$(basename "$CLONE_DIR")
  tmux split-window -t "$SESSION:1" -c "$CLONE_DIR"
  tmux select-pane -T "$CLONE_NAME"
done

# Tile panes evenly
tmux select-layout -t "$SESSION:1" tiled

# Launch Claude Code in each pane
for ((i=0; i<NUM_CLONES; i++)); do
  PANE_NUM=$((i + 1))
  tmux send-keys -t "$SESSION:1.${PANE_NUM}" "claude" Enter
done

echo "Session '$SESSION' created with $NUM_CLONES agents."
echo "Attach with: tmux attach -t $SESSION"
echo ""
echo "Shortcuts inside tmux:"
echo "  Alt+Arrow  - Navigate panes"
echo "  Alt+z      - Zoom/unzoom pane"
echo "  Alt+b      - Broadcast to all panes"
echo "  Alt+=      - Re-tile panes evenly"
echo "  Alt+x      - Kill current pane"