Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ GEMINI.md
openspec
*.log
.hippocampus
config/local/*
!config/local/.gitkeep
115 changes: 114 additions & 1 deletion ccb
Original file line number Diff line number Diff line change
Expand Up @@ -4292,6 +4292,115 @@ def cmd_reinstall(_args) -> int:
return _run_installer("install")


CONFIG_TEMPLATES = [
"claude-md-ccb.md",
"agents-md-ccb.md",
"clinerules-ccb.md",
]


def cmd_config(argv: list[str]) -> int:
"""Handle ccb config subcommands for local config overrides."""
parser = argparse.ArgumentParser(
prog="ccb config",
description="Manage local config overrides that persist across updates",
)
subparsers = parser.add_subparsers(dest="subcommand", help="Config subcommands")
subparsers.add_parser("show", help="Show which configs have local overrides")
init_parser = subparsers.add_parser(
"init", help="Create local override from current template"
)
init_parser.add_argument(
"template",
nargs="?",
choices=CONFIG_TEMPLATES,
help="Template to override (default: all)",
)
edit_parser = subparsers.add_parser(
"edit", help="Edit a local override in your $EDITOR"
)
edit_parser.add_argument(
"template", choices=CONFIG_TEMPLATES, help="Template to edit"
)
reset_parser = subparsers.add_parser(
"reset", help="Remove local override, reverting to upstream default"
)
reset_parser.add_argument(
"template",
nargs="?",
choices=CONFIG_TEMPLATES,
help="Template to reset (default: all)",
)

args = parser.parse_args(argv)

local_dir = script_dir / "config" / "local"

if args.subcommand == "show":
print("Local config overrides:")
print(f" Directory: {local_dir}")
print()
for tpl in CONFIG_TEMPLATES:
local_file = local_dir / tpl
default_file = script_dir / "config" / tpl
if local_file.exists():
print(f" ✅ {tpl} (local override active)")
elif default_file.exists():
print(f" ── {tpl} (using upstream default)")
else:
print(f" ❌ {tpl} (template not found)")
return 0

if args.subcommand == "init":
local_dir.mkdir(parents=True, exist_ok=True)
templates = [args.template] if args.template else CONFIG_TEMPLATES
for tpl in templates:
src = script_dir / "config" / tpl
dst = local_dir / tpl
if not src.exists():
print(f" WARN: Template not found: {src}", file=sys.stderr)
continue
if dst.exists():
print(f" Skip: {tpl} (local override already exists)")
continue
shutil.copy2(str(src), str(dst))
print(f" Created: config/local/{tpl}")
print()
print("Edit your local overrides, then run 'ccb reinstall' to apply.")
return 0

if args.subcommand == "edit":
local_file = local_dir / args.template
if not local_file.exists():
# Auto-init if not present
local_dir.mkdir(parents=True, exist_ok=True)
src = script_dir / "config" / args.template
if not src.exists():
print(f"Template not found: {src}", file=sys.stderr)
return 1
shutil.copy2(str(src), str(local_file))
print(f"Created config/local/{args.template} from upstream template.")

editor = os.environ.get("EDITOR", "vi")
return subprocess.call([editor, str(local_file)])

if args.subcommand == "reset":
templates = [args.template] if args.template else CONFIG_TEMPLATES
for tpl in templates:
local_file = local_dir / tpl
if local_file.exists():
local_file.unlink()
print(f" Removed: config/local/{tpl}")
else:
print(f" Skip: {tpl} (no local override)")
print()
print("Run 'ccb reinstall' to apply upstream defaults.")
return 0

parser.print_help()
return 1


def _droid_server_path() -> Path:
return script_dir / "mcp" / "ccb-delegation" / "server.py"

Expand Down Expand Up @@ -4510,6 +4619,10 @@ def main():
if argv and argv[0] == "droid" and len(argv) > 1 and argv[1] in {"setup-delegation", "test-delegation"}:
return cmd_droid_subcommand(argv[1:])

# Handle 'ccb config' subcommand
if argv and argv[0] == "config":
return cmd_config(argv[1:])

if argv and argv[0] in {"kill", "update", "version", "uninstall", "reinstall"}:
parser = argparse.ArgumentParser(description="Claude AI unified launcher", add_help=True)
subparsers = parser.add_subparsers(dest="command", help="Subcommands")
Expand Down Expand Up @@ -4548,7 +4661,7 @@ def main():
start_parser = argparse.ArgumentParser(
description="Claude AI unified launcher",
add_help=True,
epilog="Other commands: ccb update | ccb version | ccb kill | ccb uninstall | ccb reinstall | ccb droid setup-delegation | ccb mail setup",
epilog="Other commands: ccb update | ccb version | ccb kill | ccb config | ccb uninstall | ccb reinstall | ccb droid setup-delegation | ccb mail setup",
)
start_parser.add_argument(
"providers",
Expand Down
Empty file added config/local/.gitkeep
Empty file.
84 changes: 84 additions & 0 deletions docs/custom-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Custom Config Overrides

CCB ships with default config templates in `config/`. These templates define:

- **Role assignments** (which AI handles which role)
- **Review framework** (peer review rules and rubrics)
- **Collaboration rules** (async guardrails, inspiration consultation)

By default, `ccb update` downloads the latest version and overwrites these
templates. If you customize roles or review rules, your changes are lost.

## Local Overrides

The `config/local/` directory lets you persist custom configurations across
updates. Files placed here take priority over the upstream defaults.

### Supported override files

| File | Injected into | Controls |
|------|--------------|----------|
| `claude-md-ccb.md` | `~/.claude/CLAUDE.md` | Role table, review framework, collaboration rules |
| `agents-md-ccb.md` | `AGENTS.md` | Role table, review rubrics for Codex |
| `clinerules-ccb.md` | `.clinerules` | Role table for Cline/Roo |

### Quick start

```bash
# Initialize local overrides from current upstream templates
ccb config init

# Edit a specific override
ccb config edit claude-md-ccb.md

# Apply changes
ccb reinstall

# Check which overrides are active
ccb config show

# Remove a local override (revert to upstream)
ccb config reset claude-md-ccb.md
```

### Example: Custom role assignments

1. Initialize the override:
```bash
ccb config init claude-md-ccb.md
```

2. Edit `config/local/claude-md-ccb.md` and change the role table:
```markdown
| Role | Provider | Description |
|------|----------|-------------|
| `architect` | `claude` | Primary planner, orchestrator, final acceptance |
| `executor` | `codex` | Code implementation, testing, bug fixing |
| `reviewer` | `gemini` | Code review, quality assessment |
```

3. Apply:
```bash
ccb reinstall
```

4. Future `ccb update` commands will preserve your `config/local/` directory
and continue using your custom role assignments.

## How it works

- `copy_project()` in `install.sh` saves and restores `config/local/` during
updates, so the directory survives even full version upgrades.
- `install_*_config()` functions check for a local override before falling
back to the upstream default template.
- The `ccb config` CLI provides convenient management commands.

## Important notes

- Local overrides replace the **entire** template file, not individual
sections. If upstream adds new sections, you may need to merge them
manually. Run `ccb config show` after updates to check.
- To see what changed upstream, compare your override with the default:
```bash
diff config/local/claude-md-ccb.md config/claude-md-ccb.md
```
40 changes: 37 additions & 3 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -561,11 +561,27 @@ copy_project() {
-cf - . | tar -C "$staging" -xf -
fi

# Preserve user's local config overrides across updates
local local_config_backup=""
if [[ -d "$INSTALL_PREFIX/config/local" ]]; then
local_config_backup="$(mktemp -d)"
cp -a "$INSTALL_PREFIX/config/local" "$local_config_backup/local"
echo " Preserving config/local/ overrides..."
fi

rm -rf "$INSTALL_PREFIX"
mkdir -p "$(dirname "$INSTALL_PREFIX")"
mv "$staging" "$INSTALL_PREFIX"
trap - EXIT

# Restore user's local config overrides
if [[ -n "$local_config_backup" && -d "$local_config_backup/local" ]]; then
mkdir -p "$INSTALL_PREFIX/config"
cp -a "$local_config_backup/local" "$INSTALL_PREFIX/config/local"
rm -rf "$local_config_backup"
echo " Restored config/local/ overrides."
fi

# Update GIT_COMMIT and GIT_DATE in ccb file
local git_commit="" git_date=""

Expand Down Expand Up @@ -968,9 +984,25 @@ except Exception as e:
fi
}

# Resolve config template: prefer config/local/ override, fallback to default.
# Prints the resolved path to stdout. Info messages go to stderr.
resolve_config_template() {
local template_name="$1"
local local_override="$INSTALL_PREFIX/config/local/$template_name"
local default_template="$INSTALL_PREFIX/config/$template_name"

if [[ -f "$local_override" ]]; then
echo " Using local override: config/local/$template_name" >&2
echo "$local_override"
else
echo "$default_template"
fi
}

install_claude_md_config() {
local claude_md="$HOME/.claude/CLAUDE.md"
local template="$INSTALL_PREFIX/config/claude-md-ccb.md"
local template
template="$(resolve_config_template claude-md-ccb.md)"
mkdir -p "$HOME/.claude"
if ! pick_python_bin; then
echo "ERROR: python required to update CLAUDE.md"
Expand Down Expand Up @@ -1040,7 +1072,8 @@ CCB_RUBRICS_END_MARKER="<!-- REVIEW_RUBRICS_END -->"

install_agents_md_config() {
local agents_md="$INSTALL_PREFIX/AGENTS.md"
local template="$INSTALL_PREFIX/config/agents-md-ccb.md"
local template
template="$(resolve_config_template agents-md-ccb.md)"

if ! pick_python_bin; then
echo "WARN: python required to update AGENTS.md; skipping"
Expand Down Expand Up @@ -1092,7 +1125,8 @@ with open(sys.argv[1], 'w', encoding='utf-8') as f:

install_clinerules_config() {
local clinerules="$INSTALL_PREFIX/.clinerules"
local template="$INSTALL_PREFIX/config/clinerules-ccb.md"
local template
template="$(resolve_config_template clinerules-ccb.md)"

if ! pick_python_bin; then
echo "WARN: python required to update .clinerules; skipping"
Expand Down
Loading