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
12 changes: 6 additions & 6 deletions .clinerules
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
Abstract roles map to concrete AI providers. Skills reference roles, not providers directly.

| Role | Provider | Description |
|------|----------|-------------|
| `designer` | `claude` | Primary planner and architect — owns plans and designs |
| `inspiration` | `gemini` | Creative brainstorming — provides ideas as reference only (unreliable, never blindly follow) |
| `reviewer` | `codex` | Scored quality gate — evaluates plans/code using Rubrics |
| `executor` | `claude` | Code implementation — writes and modifies code |
|---|---|---|
| `designer` | `claude-opus` | Primary planner and architect — owns plans and designs |
| `inspiration` | `gemini` | Task-conditioned second perspective — architectural challenge (default) or creative brainstorming (for UI/UX/naming/ideation tasks) |
| `reviewer` | `claude-sonnet`, `codex` | Both review and evaluate — all dimensions must score 10 |
| `executor` | `claude-opus` | Code implementation — writes and modifies code |

To change a role assignment, edit the Provider column above.
When a skill references a role (e.g. `reviewer`), resolve it to the provider listed here.
When a skill references a role (e.g. `reviewer`), resolve it to BOTH providers listed (send to each via `/ask`).
<!-- CCB_ROLES_END -->
40 changes: 31 additions & 9 deletions bin/ask
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Usage:
ask <provider> [options] <message>

Providers:
gemini, codex, opencode, droid, claude, copilot, codebuddy, qwen
gemini, codex, opencode, droid, claude, claude-opus, claude-sonnet, copilot, codebuddy, qwen

Modes:
Default (async): Background task with hook callback
Expand All @@ -32,6 +32,8 @@ from pathlib import Path

PROVIDER_DISPLAY = {
"opencode": "OpenCode",
"claude-opus": "Claude-Opus",
"claude-sonnet": "Claude-Sonnet",
}


Expand All @@ -46,7 +48,7 @@ from compat import read_stdin_text, setup_windows_encoding
setup_windows_encoding()

from cli_output import EXIT_ERROR, EXIT_OK
from providers import parse_qualified_provider
from providers import parse_qualified_provider, make_qualified_key
from session_utils import find_project_session_file


Expand All @@ -57,13 +59,17 @@ PROVIDER_DAEMONS = {
"opencode": "oask",
"droid": "dask",
"claude": "lask",
"claude-opus": "loask",
"claude-sonnet": "lsask",
"copilot": "hask",
"codebuddy": "bask",
"qwen": "qask",
}

CALLER_SESSION_FILES = {
"claude": ".claude-session",
"claude-opus": ".claude-opus-session",
"claude-sonnet": ".claude-sonnet-session",
"codex": ".codex-session",
"gemini": ".gemini-session",
"opencode": ".opencode-session",
Expand All @@ -77,6 +83,8 @@ CALLER_PANE_ENV_HINTS = {
"codex": ("CODEX_TMUX_SESSION", "CODEX_WEZTERM_PANE"),
"gemini": ("GEMINI_TMUX_SESSION", "GEMINI_WEZTERM_PANE"),
"opencode": ("OPENCODE_TMUX_SESSION", "OPENCODE_WEZTERM_PANE"),
"claude-opus": ("CLAUDE_OPUS_TMUX_SESSION", "CLAUDE_OPUS_WEZTERM_PANE"),
"claude-sonnet": ("CLAUDE_SONNET_TMUX_SESSION", "CLAUDE_SONNET_WEZTERM_PANE"),
"droid": ("DROID_TMUX_SESSION", "DROID_WEZTERM_PANE"),
"copilot": ("COPILOT_TMUX_SESSION", "COPILOT_WEZTERM_PANE"),
"codebuddy": ("CODEBUDDY_TMUX_SESSION", "CODEBUDDY_WEZTERM_PANE"),
Expand All @@ -87,6 +95,8 @@ CALLER_ENV_HINTS = {
"codex": ("CODEX_SESSION_ID", "CODEX_RUNTIME_DIR"),
"gemini": ("GEMINI_SESSION_ID", "GEMINI_RUNTIME_DIR"),
"opencode": ("OPENCODE_SESSION_ID", "OPENCODE_RUNTIME_DIR"),
"claude-opus": ("CLAUDE_OPUS_SESSION_ID", "CLAUDE_OPUS_RUNTIME_DIR"),
"claude-sonnet": ("CLAUDE_SONNET_SESSION_ID", "CLAUDE_SONNET_RUNTIME_DIR"),
"droid": ("DROID_SESSION_ID", "DROID_RUNTIME_DIR"),
"copilot": ("COPILOT_SESSION_ID", "COPILOT_RUNTIME_DIR"),
"codebuddy": ("CODEBUDDY_SESSION_ID", "CODEBUDDY_RUNTIME_DIR"),
Expand Down Expand Up @@ -499,7 +509,7 @@ def _usage() -> None:
print("Usage: ask <provider> [options] <message>", file=sys.stderr)
print("", file=sys.stderr)
print("Providers:", file=sys.stderr)
print(" gemini, codex, opencode, droid, claude, copilot, codebuddy, qwen", file=sys.stderr)
print(" gemini, codex, opencode, droid, claude, claude-opus, claude-sonnet, copilot, codebuddy, qwen", file=sys.stderr)
print("", file=sys.stderr)
print("Options:", file=sys.stderr)
print(" -h, --help Show this help message", file=sys.stderr)
Expand All @@ -524,13 +534,19 @@ def main(argv: list[str]) -> int:

base_provider, instance = parse_qualified_provider(raw_provider)

# When running inside a named CCB session, qualify the provider so the daemon
# routes to the session-specific provider pane and session file.
session_name = os.environ.get("CCB_SESSION_NAME", "").strip()
if not instance and session_name:
instance = session_name

if base_provider not in PROVIDER_DAEMONS:
print(f"[ERROR] Unknown provider: {base_provider}", file=sys.stderr)
print(f"[ERROR] Available: {', '.join(PROVIDER_DAEMONS.keys())}", file=sys.stderr)
return EXIT_ERROR

daemon_cmd = PROVIDER_DAEMONS[base_provider]
provider = raw_provider # keep full qualified key for daemon routing
provider = make_qualified_key(base_provider, instance)

# Parse remaining arguments
timeout: float = 3600.0
Expand Down Expand Up @@ -634,8 +650,10 @@ def main(argv: list[str]) -> int:
task_id = make_task_id()
log_dir = Path(tempfile.gettempdir()) / "ccb-tasks"
log_dir.mkdir(parents=True, exist_ok=True)
log_file = log_dir / f"ask-{provider}-{task_id}.log"
status_file = log_dir / f"ask-{provider}-{task_id}.status"
# Sanitize provider for filenames — colon is invalid on Windows.
_safe_provider = provider.replace(":", "-")
log_file = log_dir / f"ask-{_safe_provider}-{task_id}.log"
status_file = log_dir / f"ask-{_safe_provider}-{task_id}.status"
try:
log_file.touch(exist_ok=True)
except Exception:
Expand Down Expand Up @@ -665,11 +683,11 @@ def main(argv: list[str]) -> int:
CREATE_NEW_PROCESS_GROUP = 0x00000200

# Write message to temp file to avoid escaping issues
msg_file = log_dir / f"ask-{provider}-{task_id}.msg"
msg_file = log_dir / f"ask-{_safe_provider}-{task_id}.msg"
msg_file.write_text(message, encoding="utf-8")

# Write PowerShell script - call ask --foreground to use unified daemon
script_file = log_dir / f"ask-{provider}-{task_id}.ps1"
script_file = log_dir / f"ask-{_safe_provider}-{task_id}.ps1"
status_file_win = str(status_file).replace('"', '`"')
log_file_win = str(log_file).replace('"', '`"')

Expand All @@ -691,6 +709,8 @@ def main(argv: list[str]) -> int:
win_pane_env_lines += f'$env:CCB_CALLER_PANE_ID = "{win_caller_pane_id}"\n'
if win_caller_terminal:
win_pane_env_lines += f'$env:CCB_CALLER_TERMINAL = "{win_caller_terminal}"\n'
if session_name:
win_pane_env_lines += f'$env:CCB_SESSION_NAME = "{session_name}"\n'

script_content = f'''$ErrorActionPreference = "SilentlyContinue"
$OutputEncoding = [System.Text.Encoding]::UTF8
Expand Down Expand Up @@ -745,6 +765,8 @@ exit $rc
pane_env_lines += f'export CCB_CALLER_PANE_ID="{bg_pane_id}"\n'
if bg_terminal:
pane_env_lines += f'export CCB_CALLER_TERMINAL="{bg_terminal}"\n'
if session_name:
pane_env_lines += f'export CCB_SESSION_NAME="{session_name}"\n'

quoted_status = shlex.quote(str(status_file))
quoted_ask_cmd = shlex.quote(ask_cmd)
Expand All @@ -771,7 +793,7 @@ fi
exit "$rc"
'''
# Write script to temp file for detached execution
script_file = log_dir / f"ask-{provider}-{task_id}.sh"
script_file = log_dir / f"ask-{_safe_provider}-{task_id}.sh"
script_file.write_text(bg_script, encoding="utf-8")
script_file.chmod(0o755)

Expand Down
6 changes: 4 additions & 2 deletions bin/askd
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ from askd.adapters.codex import CodexAdapter
from askd.adapters.gemini import GeminiAdapter
from askd.adapters.opencode import OpenCodeAdapter
from askd.adapters.droid import DroidAdapter
from askd.adapters.claude import ClaudeAdapter
from askd.adapters.claude import ClaudeAdapter, ClaudeOpusAdapter, ClaudeSonnetAdapter
from askd.adapters.copilot import CopilotAdapter
from askd.adapters.codebuddy import CodebuddyAdapter
from askd.adapters.qwen import QwenAdapter
Expand All @@ -41,14 +41,16 @@ def _parse_listen(value: str) -> tuple[str, int]:
return host or "127.0.0.1", int(port_s or "0")


ALL_PROVIDERS = ["codex", "gemini", "opencode", "droid", "claude", "copilot", "codebuddy", "qwen"]
ALL_PROVIDERS = ["codex", "gemini", "opencode", "droid", "claude", "claude-opus", "claude-sonnet", "copilot", "codebuddy", "qwen"]

ADAPTER_CLASSES = {
"codex": CodexAdapter,
"gemini": GeminiAdapter,
"opencode": OpenCodeAdapter,
"droid": DroidAdapter,
"claude": ClaudeAdapter,
"claude-opus": ClaudeOpusAdapter,
"claude-sonnet": ClaudeSonnetAdapter,
"copilot": CopilotAdapter,
"codebuddy": CodebuddyAdapter,
"qwen": QwenAdapter,
Expand Down
7 changes: 6 additions & 1 deletion bin/ccb-completion-hook
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ from completion_hook import (
default_reply_for_status,
normalize_completion_status,
)
from providers import session_filename_for_instance
from session_utils import find_project_session_file

setup_windows_encoding()
Expand Down Expand Up @@ -559,12 +560,16 @@ def main() -> int:
# Fallback: find caller's pane_id from session file
session_files = {
"claude": ".claude-session",
"claude-opus": ".claude-opus-session",
"claude-sonnet": ".claude-sonnet-session",
"codex": ".codex-session",
"gemini": ".gemini-session",
"opencode": ".opencode-session",
"droid": ".droid-session",
}
session_filename = session_files.get(caller, ".claude-session")
base_session_filename = session_files.get(caller, ".claude-session")
ccb_session_name = os.environ.get("CCB_SESSION_NAME", "").strip()
session_filename = session_filename_for_instance(base_session_filename, ccb_session_name or None)

work_dir = os.environ.get("CCB_WORK_DIR", "")
search_paths: list[Path] = []
Expand Down
8 changes: 7 additions & 1 deletion bin/ccb-ping
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ PROVIDER_COMMS = {
"opencode": ("opencode_comm", "OpenCodeCommunicator"),
"droid": ("droid_comm", "DroidCommunicator"),
"claude": ("claude_comm", "ClaudeCommunicator"),
"claude-opus": ("claude_comm", "ClaudeCommunicator"),
"claude-sonnet": ("claude_comm", "ClaudeCommunicator"),
}

def _usage():
print("Usage: ccb-ping <provider> [--session-file FILE] [--autostart]", file=sys.stderr)
print("", file=sys.stderr)
print("Providers:", file=sys.stderr)
print(" gemini, codex, opencode, droid, claude", file=sys.stderr)
print(" gemini, codex, opencode, droid, claude, claude-opus, claude-sonnet", file=sys.stderr)


def _resolve_work_dir(session_file: str | None) -> Path:
Expand Down Expand Up @@ -96,6 +98,8 @@ def main():
DASK_CLIENT_SPEC,
GASK_CLIENT_SPEC,
LASK_CLIENT_SPEC,
LOASK_CLIENT_SPEC,
LSASK_CLIENT_SPEC,
OASK_CLIENT_SPEC,
)

Expand All @@ -104,6 +108,8 @@ def main():
"gemini": GASK_CLIENT_SPEC,
"opencode": OASK_CLIENT_SPEC,
"claude": LASK_CLIENT_SPEC,
"claude-opus": LOASK_CLIENT_SPEC,
"claude-sonnet": LSASK_CLIENT_SPEC,
"droid": DASK_CLIENT_SPEC,
}

Expand Down
5 changes: 3 additions & 2 deletions bin/cpend
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ try:
from pane_registry import load_registry_by_session_id, load_registry_by_claude_pane, load_registry_by_project_id
from session_utils import find_project_session_file
from askd_client import resolve_work_dir_with_registry
from providers import CASK_CLIENT_SPEC
from providers import CASK_CLIENT_SPEC, session_filename_for_instance
from project_id import compute_ccb_project_id
except ImportError as exc:
print(f"Import failed: {exc}")
Expand All @@ -42,7 +42,8 @@ def _debug(message: str) -> None:

def _load_session_log_path(work_dir: Path, explicit_session_file: Path | None) -> tuple[Path | None, str | None]:
"""Load codex_session_path from .codex-session (or .ccb/.codex-session) if exists"""
session_file = explicit_session_file or find_project_session_file(work_dir, ".codex-session")
_sfn = session_filename_for_instance(".codex-session", os.environ.get("CCB_SESSION_NAME", "").strip() or None)
session_file = explicit_session_file or find_project_session_file(work_dir, _sfn)
if not session_file:
return None, None
try:
Expand Down
10 changes: 7 additions & 3 deletions bin/dpend
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ from cli_output import EXIT_ERROR, EXIT_NO_REPLY, EXIT_OK
from droid_comm import DroidLogReader, read_droid_session_start
from ccb_protocol import strip_trailing_markers
from askd_client import resolve_work_dir_with_registry
from providers import DASK_CLIENT_SPEC
from providers import DASK_CLIENT_SPEC, session_filename_for_instance
from session_utils import find_project_session_file, safe_write_session
from project_id import compute_ccb_project_id
from pane_registry import load_registry_by_project_id
Expand All @@ -36,8 +36,12 @@ def _debug(message: str) -> None:
print(f"[DEBUG] {message}", file=sys.stderr)


def _session_filename() -> str:
return session_filename_for_instance(".droid-session", os.environ.get("CCB_SESSION_NAME", "").strip() or None)


def _load_session_path(work_dir: Path) -> Path | None:
session_file = find_project_session_file(work_dir, ".droid-session")
session_file = find_project_session_file(work_dir, _session_filename())
if not session_file:
return None
try:
Expand All @@ -52,7 +56,7 @@ def _load_session_path(work_dir: Path) -> Path | None:


def _update_session_file(work_dir: Path, actual_session: Path) -> None:
session_file = find_project_session_file(work_dir, ".droid-session")
session_file = find_project_session_file(work_dir, _session_filename())
if not session_file:
return
try:
Expand Down
10 changes: 7 additions & 3 deletions bin/gpend
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ try:
from cli_output import EXIT_ERROR, EXIT_NO_REPLY, EXIT_OK
from gemini_comm import GeminiLogReader
from askd_client import resolve_work_dir_with_registry
from providers import GASK_CLIENT_SPEC
from providers import GASK_CLIENT_SPEC, session_filename_for_instance
from session_utils import find_project_session_file
from project_id import compute_ccb_project_id
from pane_registry import load_registry_by_project_id
Expand All @@ -40,9 +40,13 @@ def _debug(message: str) -> None:
print(f"[DEBUG] {message}", file=sys.stderr)


def _session_filename() -> str:
return session_filename_for_instance(".gemini-session", os.environ.get("CCB_SESSION_NAME", "").strip() or None)


def _load_session_path(work_dir: Path) -> Path | None:
"""Load gemini_session_path from .gemini-session (or .ccb/.gemini-session) if exists"""
session_file = find_project_session_file(work_dir, ".gemini-session")
session_file = find_project_session_file(work_dir, _session_filename())
if not session_file:
return None
try:
Expand All @@ -58,7 +62,7 @@ def _load_session_path(work_dir: Path) -> Path | None:

def _update_session_file(work_dir: Path, actual_session: Path) -> None:
"""Update .gemini-session with actual session path if different"""
session_file = find_project_session_file(work_dir, ".gemini-session")
session_file = find_project_session_file(work_dir, _session_filename())
if not session_file:
return
try:
Expand Down
Loading