From 3eca60148c3ca7bcde017f0492f18f174a183519 Mon Sep 17 00:00:00 2001 From: rob Date: Sat, 1 Nov 2025 14:42:04 -0300 Subject: [PATCH] feat: align ramble defaults with shared ai config --- assets/runtime/create_feature.py | 44 ++++++++++- assets/runtime/ramble.py | 124 ++++++++++++++++++++++++++----- config/ai.yml | 2 +- 3 files changed, 146 insertions(+), 24 deletions(-) diff --git a/assets/runtime/create_feature.py b/assets/runtime/create_feature.py index c171fce..513cd7e 100644 --- a/assets/runtime/create_feature.py +++ b/assets/runtime/create_feature.py @@ -18,6 +18,11 @@ import argparse, datetime, json, os, re, subprocess, sys from pathlib import Path from typing import Dict, List, Tuple +try: + from automation.ai_config import load_ai_settings +except ImportError: + load_ai_settings = None # type: ignore + # --------- helpers --------- def say(msg: str) -> None: print(msg, flush=True) @@ -204,14 +209,47 @@ def main(): ap.add_argument("--dir", help="Repo root (defaults to git root or CWD)") ap.add_argument("--title", help="Feature title (useful without Ramble)") ap.add_argument("--no-ramble", action="store_true", help="Disable Ramble UI") - ap.add_argument("--provider", choices=["mock", "claude"], default="mock") - ap.add_argument("--claude-cmd", default="claude") + ap.add_argument("--provider", help="Override Ramble provider (defaults to config)") + ap.add_argument("--claude-cmd", help="Override claude CLI path") args = ap.parse_args() start = Path(args.dir).expanduser().resolve() if args.dir else Path.cwd() repo = git_root_or_cwd(start) say(f"[=] Using repository: {repo}") + provider = args.provider + claude_cmd = args.claude_cmd + provider_map: Dict[str, Dict[str, object]] = {} + default_provider = "mock" + + if load_ai_settings is not None: + try: + settings = load_ai_settings(repo) + provider_map = dict(settings.ramble.providers) + candidate_default = settings.ramble.default_provider + if isinstance(candidate_default, str) and candidate_default.strip(): + default_provider = candidate_default.strip() + except Exception: + provider_map = {} + + if "mock" not in provider_map: + provider_map["mock"] = {"kind": "mock"} + + if not provider: + provider = default_provider if default_provider in provider_map else "mock" + elif provider not in provider_map: + say(f"[WARN] Unknown Ramble provider '{provider}', defaulting to {default_provider}") + provider = default_provider if default_provider in provider_map else "mock" + + if not claude_cmd: + selected = provider_map.get(provider, {}) + maybe_cmd = selected.get("command") if isinstance(selected, dict) else None + if isinstance(maybe_cmd, str) and maybe_cmd.strip(): + claude_cmd = maybe_cmd.strip() + + if not claude_cmd: + claude_cmd = "claude" + tmpl_path = repo / "process" / "templates" / "feature_request.md" tmpl = read_text(tmpl_path) parsed_fields = find_template_fields(tmpl) or [(f, "") for f in default_fields()] @@ -222,7 +260,7 @@ def main(): # Try Ramble unless disabled fields: Dict[str, str] | None = None if not args.no_ramble: - fields = try_ramble(repo, field_labels, provider=args.provider, claude_cmd=args.claude_cmd) + fields = try_ramble(repo, field_labels, provider=provider, claude_cmd=claude_cmd) # Terminal prompts fallback if not fields: diff --git a/assets/runtime/ramble.py b/assets/runtime/ramble.py index ad9040e..af82ea0 100644 --- a/assets/runtime/ramble.py +++ b/assets/runtime/ramble.py @@ -35,6 +35,7 @@ Requirements from __future__ import annotations from dataclasses import dataclass +from pathlib import Path from typing import Dict, List, Optional, Any, Tuple, Protocol, runtime_checkable, Mapping, cast import os, sys, json, textwrap, base64, re, time, shutil, subprocess, argparse, threading @@ -43,6 +44,11 @@ try: except ImportError: requests = None +try: + from automation.ai_config import load_ai_settings +except ImportError: + load_ai_settings = None # type: ignore + # ── Qt (PySide6 preferred; PyQt5 fallback) ──────────────────────────────────── QT_LIB = None try: @@ -690,38 +696,116 @@ def open_ramble_dialog( # ── CLI demo ────────────────────────────────────────────────────────────────── -def parse_args(): +def load_ramble_preferences(repo_root: Path) -> Tuple[List[str], str, Dict[str, Any], Dict[str, Dict[str, Any]]]: + """Return (choices, default_provider, claude_defaults, provider_map).""" + provider_map: Dict[str, Dict[str, Any]] = {} + default_provider = "mock" + + if load_ai_settings is not None: + try: + settings = load_ai_settings(repo_root) + provider_map = dict(settings.ramble.providers) + candidate_default = settings.ramble.default_provider + if isinstance(candidate_default, str) and candidate_default.strip(): + default_provider = candidate_default.strip() + except Exception: + provider_map = {} + + if "mock" not in provider_map: + provider_map["mock"] = {"kind": "mock"} + + provider_choices = sorted(provider_map.keys()) + if default_provider not in provider_choices: + default_provider = "mock" + + claude_defaults = provider_map.get("claude", {}) + if not isinstance(claude_defaults, dict): + claude_defaults = {} + + return provider_choices, default_provider, claude_defaults, provider_map + + +def build_provider(name: str, args: "argparse.Namespace", providers: Dict[str, Dict[str, Any]]) -> RambleProvider: + meta = providers.get(name, {}) + if not isinstance(meta, dict): + meta = {} + + kind = meta.get("kind") + if not isinstance(kind, str) or not kind: + kind = "mock" if name == "mock" else name + + if kind == "mock": + return cast(RambleProvider, MockProvider()) + + if kind in {"claude", "claude_cli"}: + cmd = args.claude_cmd or meta.get("command") or "claude" + extra_args = meta.get("args", []) or [] + if isinstance(extra_args, str): + extra_args = [extra_args] + extra_args = [str(x) for x in extra_args] + + use_arg_p = bool(meta.get("use_arg_p", True)) + log_path = str(meta.get("log_path", "/tmp/ramble_claude.log")) + timeout_s = int(args.timeout) + tail_chars = int(args.tail) + debug_flag = bool(args.debug or meta.get("debug", False)) + + return cast( + RambleProvider, + ClaudeCLIProvider( + cmd=cmd, + extra_args=extra_args, + timeout_s=timeout_s, + tail_chars=tail_chars, + use_arg_p=use_arg_p, + debug=debug_flag, + log_path=log_path, + ), + ) + + raise ValueError(f"Unknown Ramble provider kind: {kind}") + + +def parse_args( + provider_choices: List[str], + default_provider: str, + claude_defaults: Dict[str, Any], +): p = argparse.ArgumentParser(description="Ramble → Generate (PlantUML + optional images)") - p.add_argument("--provider", choices=["mock", "claude"], default="mock") - p.add_argument("--claude-cmd", default="claude", help="Path to claude CLI") + p.add_argument("--provider", choices=provider_choices, default=default_provider) + + claude_cmd_default = str(claude_defaults.get("command", "claude")) + try: + timeout_default = int(claude_defaults.get("timeout_s", 90)) + except (TypeError, ValueError): + timeout_default = 90 + try: + tail_default = int(claude_defaults.get("tail_chars", 6000)) + except (TypeError, ValueError): + tail_default = 6000 + + p.add_argument("--claude-cmd", default=claude_cmd_default, help="Path to claude CLI") p.add_argument("--stability", action="store_true", help="Enable Stability AI images (needs STABILITY_API_KEY)") p.add_argument("--pexels", action="store_true", help="Enable Pexels images (needs PEXELS_API_KEY); ignored if --stability set") p.add_argument("--prompt", default="Explain your new feature idea") p.add_argument("--fields", nargs="+", default=["Summary","Title","Intent","ProblemItSolves","BriefOverview"]) p.add_argument("--criteria", default="", help="JSON mapping of field -> criteria") p.add_argument("--hints", default="", help="JSON list of hint strings") - p.add_argument("--timeout", type=int, default=90) - p.add_argument("--tail", type=int, default=6000) + p.add_argument("--timeout", type=int, default=timeout_default) + p.add_argument("--tail", type=int, default=tail_default) p.add_argument("--debug", action="store_true") return p.parse_args() if __name__ == "__main__": - args = parse_args() + repo_root = Path.cwd() + provider_choices, default_provider, claude_defaults, provider_map = load_ramble_preferences(repo_root) + args = parse_args(provider_choices, default_provider, claude_defaults) - # Field criteria parsing - criteria: Dict[str, str] = {} - if args.criteria.strip(): - try: - criteria = json.loads(args.criteria) - except Exception as e: - print(f"[WARN] Could not parse --criteria JSON: {e}", file=sys.stderr) - - # Provider - if args.provider == "claude": - provider = cast(RambleProvider, ClaudeCLIProvider( - cmd=args.claude_cmd, use_arg_p=True, timeout_s=args.timeout, tail_chars=args.tail, debug=args.debug, - )) - else: + # Provider selection with fallback + try: + provider = build_provider(args.provider, args, provider_map) + except Exception as exc: + print(f"[WARN] Ramble provider '{args.provider}' unavailable ({exc}); falling back to mock", file=sys.stderr) provider = cast(RambleProvider, MockProvider()) # Ensure PlantUML diff --git a/config/ai.yml b/config/ai.yml index ab7a528..5bd26d5 100644 --- a/config/ai.yml +++ b/config/ai.yml @@ -24,7 +24,7 @@ ramble: providers: mock: kind: mock - claude_cli: + claude: kind: claude_cli command: "claude" args: []