Utility Commands
Utility commands and scripts: /cws-submit, /ccgm-sync (reverse-sync local config to CCGM), /user-test, statusline, agent-team launcher.
Tags
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.
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"