From 4df21cbb5c0cf31b77891a04dd4be9359ddb63ef Mon Sep 17 00:00:00 2001 From: bookandlover <61039415+bookandlover@users.noreply.github.com> Date: Sat, 28 Feb 2026 13:08:16 +0800 Subject: [PATCH] feat: add config/local/ overlay for persistent custom configurations Problem: `ccb update` downloads the latest release and overwrites all config templates (role table, review framework, collaboration rules). Users who customize these settings lose their changes on every update. Solution: Introduce a `config/local/` directory that acts as a persistent overlay layer. Files placed here take priority over upstream defaults and survive updates. Changes: - install.sh: copy_project() saves and restores config/local/ during updates - install.sh: add resolve_config_template() helper; config install functions check for local override before falling back to upstream default - ccb: add `ccb config` subcommand (init/edit/show/reset) for managing local overrides - docs/custom-config.md: usage documentation and examples - .gitignore: ignore user override files but track .gitkeep Co-Authored-By: Claude Opus 4.6 --- .gitignore | 2 + ccb | 115 +++++++++++++++++++++++++++++++++++++++++- config/local/.gitkeep | 0 docs/custom-config.md | 84 ++++++++++++++++++++++++++++++ install.sh | 40 +++++++++++++-- 5 files changed, 237 insertions(+), 4 deletions(-) create mode 100644 config/local/.gitkeep create mode 100644 docs/custom-config.md diff --git a/.gitignore b/.gitignore index 5ff8d5c..4590927 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ GEMINI.md openspec *.log .hippocampus +config/local/* +!config/local/.gitkeep diff --git a/ccb b/ccb index 40d01dc..09a82db 100755 --- a/ccb +++ b/ccb @@ -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" @@ -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") @@ -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", diff --git a/config/local/.gitkeep b/config/local/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/custom-config.md b/docs/custom-config.md new file mode 100644 index 0000000..a0fd06a --- /dev/null +++ b/docs/custom-config.md @@ -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 + ``` diff --git a/install.sh b/install.sh index 56bc0cf..53b9fcd 100755 --- a/install.sh +++ b/install.sh @@ -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="" @@ -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" @@ -1040,7 +1072,8 @@ CCB_RUBRICS_END_MARKER="" 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" @@ -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"