diff --git a/automation/agents.py b/automation/agents.py index ae891ea..a8c0bd3 100644 --- a/automation/agents.py +++ b/automation/agents.py @@ -39,7 +39,7 @@ def get_ai_config() -> dict[str, str]: Returns dict with 'provider' and 'command' keys. """ - # Try environment first + # Try the environment first provider = os.environ.get("CDEV_AI_PROVIDER") command = os.environ.get("CDEV_AI_COMMAND") @@ -88,12 +88,12 @@ def call_ai_cli(prompt: str, content: str) -> dict[str, Any] | None: config = get_ai_config() command_template = config["command"] - # Create temporary file in .git/ai-agents-temp/ (accessible to Claude CLI) + # Create a temporary file in .git/ai-agents-temp/ (accessible to Claude CLI) # Using .git/ ensures it's gitignored and Claude has permission temp_dir = Path.cwd() / ".git" / "ai-agents-temp" temp_dir.mkdir(parents=True, exist_ok=True) - # Use PID for unique filename to avoid conflicts + # Use PID for a unique filename to avoid conflicts content_file = temp_dir / f"discussion-{os.getpid()}.md" content_file.write_text(content, encoding='utf-8') @@ -211,7 +211,7 @@ def call_ai_api(prompt: str, content: str) -> dict[str, Any] | None: def call_ai(prompt: str, content: str) -> dict[str, Any] | None: """ - Call configured AI provider (CLI or API). + Call a configured AI provider (CLI or API). First tries CLI-based provider (claude, gemini, codex). Falls back to direct API if ANTHROPIC_API_KEY is set. @@ -241,9 +241,9 @@ def call_ai(prompt: str, content: str) -> dict[str, Any] | None: def normalize_discussion(content: str) -> dict[str, Any] | None: """ - Agent 1: Normalize natural language discussion to structured format. + Agent 1: Normalize natural language discussion to a structured format. - Extracts votes, questions, decisions, action items in consistent format. + Extracts votes, questions, decisions, action items in a consistent format. """ prompt = """You are a discussion normalizer. Extract structured information from the discussion content. diff --git a/automation/runner.py b/automation/runner.py index 4d96964..901ced5 100644 --- a/automation/runner.py +++ b/automation/runner.py @@ -23,6 +23,9 @@ def get_staged_files(repo_root: Path) -> list[Path]: """ Return staged (added/modified) paths relative to the repository root. """ + # We only care about what is in the index; the working tree may include + # experiments the developer does not intend to commit. `--diff-filter=AM` + # narrows the list to new or modified files. result = run( ["git", "diff", "--cached", "--name-only", "--diff-filter=AM"], cwd=repo_root, @@ -42,18 +45,21 @@ def merge_instructions(source_instr: str, output_instr: str, append_instr: str) if append_instr: prefix = (final + "\n\n") if final else "" final = f"{prefix}Additional requirements for this output location:\n{append_instr}" - return final.strip() + return final.strip() # Final, human-readable instruction block handed to the AI def process(repo_root: Path, rules: RulesConfig, model: ModelConfig) -> int: """ Walk staged files, resolve matching outputs, and invoke the patcher for each. """ + # 1) Gather the staged file list (Git index only). staged_files = get_staged_files(repo_root) if not staged_files: return 0 + # 2) For each staged file, look up the matching rule and iterate outputs. for src_rel in staged_files: + # Find the most specific rule (nearest .ai-rules.yml wins). rule_name = rules.get_rule_name(src_rel) if not rule_name: continue @@ -79,6 +85,9 @@ def process(repo_root: Path, rules: RulesConfig, model: ModelConfig) -> int: print(f"[runner] skipping {output_name}: unsafe path {rendered_path}", file=sys.stderr) continue + # Build the instruction set for this output. Output-specific text + # overrides the rule-level text, and we keep the source version as a + # fallback. instruction = output_cfg.get("instruction", "") or source_instruction append = output_cfg.get("instruction_append", "") @@ -90,6 +99,7 @@ def process(repo_root: Path, rules: RulesConfig, model: ModelConfig) -> int: final_instruction = merge_instructions(source_instruction, instruction, append) + # 3) Ask the patcher to build a diff with the assembled instruction. try: generate_output( repo_root=repo_root, @@ -109,10 +119,12 @@ def main(argv: list[str] | None = None) -> int: """ CLI entry point used by the pre-commit hook. """ + # Parse command-line options (only --model override today). parser = argparse.ArgumentParser(description="CascadingDev AI runner") parser.add_argument("--model", help="Override AI command (default from env)") args = parser.parse_args(argv) + # Load the nearest .ai-rules.yml (fail quietly if missing). repo_root = Path.cwd().resolve() try: rules = RulesConfig.load(repo_root) @@ -120,6 +132,7 @@ def main(argv: list[str] | None = None) -> int: print("[runner] .ai-rules.yml not found; skipping") return 0 + # Instantiate the model config and delegate to the processing pipeline. model = ModelConfig(command=args.model or ModelConfig().command) return process(repo_root, rules, model)