from __future__ import annotations from datetime import datetime from pathlib import Path import typer from rich.console import Console from rich.table import Table from .discovery import append_discovery_answer, append_followup_answer, core_questions, read_discovery_answers from .handoff import build_handoff from .status import project_stage, task_counts, update_project_state, update_agent_handoff from .tasks import generate_agent_prompt, get_next_task, update_task_status from .templates import CONTEXT_FILES, write_file_if_missing from .guardrails import run_all_guardrails, format_report from .telemetry import record_event from .generate_context import generate_context_files app = typer.Typer(help="RDB discovery and delivery workflow CLI.") console = Console() def root_path() -> Path: return Path.cwd() def append_run_log(root: Path, event: str, task_id: str = "", notes: str = "") -> None: path = root / "RUN_LOG.md" if not path.exists(): path.write_text("# RUN_LOG.md\n\n| Date | Event | Task | Notes |\n|---|---|---|---|\n", encoding="utf-8") line = f"| {datetime.now().isoformat(timespec='seconds')} | {event} | {task_id} | {notes} |\n" with path.open("a", encoding="utf-8") as handle: handle.write(line) @app.command() def init() -> None: """Create the initial markdown project structure.""" root = root_path() record_event(root, "command", "rdb init") created: list[str] = [] skipped: list[str] = [] for relative_path, content in CONTEXT_FILES.items(): if write_file_if_missing(root, relative_path, content): created.append(relative_path) else: skipped.append(relative_path) console.print("[bold green]Initialised rdb-discovery files.[/bold green]") if created: console.print("\nCreated:") for item in created: console.print(f"- {item}") if skipped: console.print("\nSkipped existing files:") for item in skipped: console.print(f"- {item}") @app.command() def discover() -> None: """Ask the core discovery questions and append answers to the discovery ledger.""" root = root_path() record_event(root, "command", "rdb discover") write_file_if_missing(root, "context/discovery-log.md", CONTEXT_FILES["context/discovery-log.md"]) for index, question in enumerate(core_questions(), start=1): qid = f"Q-{index:03d}" answer = typer.prompt(question) confidence = typer.prompt("Confidence?", default="Medium") follow_up = typer.prompt("Follow-up needed?", default="No") append_discovery_answer(root, qid, question, answer, confidence, follow_up) append_run_log(root, "Discovery completed", notes="Core questions answered") console.print("[bold green]Discovery complete.[/bold green]") @app.command() def ask_more() -> None: """Ask for additional details on Low-confidence or follow-up-needed answers.""" root = root_path() record_event(root, "command", "rdb ask_more") answers = read_discovery_answers(root) if not answers: console.print("[yellow]No discovery answers found. Run `discover` first.[/yellow]") raise typer.Exit(code=0) low_conf = [a for a in answers if a["low_confidence"]] follow_ups = [a for a in answers if a["needs_followup"]] flagged: dict[str, list[str]] = {} # question_id -> list of reasons for a in low_conf: flagged.setdefault(a["id"], []).append("Low confidence") for a in follow_ups: flagged.setdefault(a["id"], []).append("Follow-up needed") if not flagged: console.print("[green]No Low-confidence or Follow-up-needed answers found.[/green]") raise typer.Exit(code=0) count = 0 for qid, reasons in sorted(flagged.items()): original = next(a for a in answers if a["id"] == qid) console.print(f"\n[yellow]Question:[/yellow] {original['question']}") console.print(f"[dim]Reasons: {', '.join(reasons)}[/dim]") fu_question = typer.prompt( f"Follow-up for {qid}", default=f"Additional detail on: {original['question'][:40]}", ) fu_answer = typer.prompt("New answer") fu_confidence = typer.prompt("Confidence?", default="Medium") append_followup_answer(root, qid, fu_question, fu_answer, fu_confidence) count += 1 console.print(f"[green]Recorded follow-up for {qid}.[/green]") console.print(f"\n[bold green]Ask-more complete: recorded {count} follow-up(s).[/bold green]") @app.command() def status() -> None: """Show current project stage and task counts.""" root = root_path() record_event(root, "command", "rdb status") console.print(f"[bold]Project stage:[/bold] {project_stage(root)}") counts = task_counts(root) table = Table(title="Task Counts") table.add_column("Status") table.add_column("Count") if counts: for status_name, count in counts.items(): table.add_row(status_name, str(count)) else: table.add_row("None", "0") console.print(table) @app.command(name="next") def next_task() -> None: """Show the next Todo task.""" root = root_path() record_event(root, "command", "rdb next") task = get_next_task(root) if not task: console.print("[yellow]No Todo task found.[/yellow]") raise typer.Exit(code=0) console.print(f"[bold]{task.task_id}[/bold] - {task.title}") console.print(f"Status: {task.status}") console.print(task.body) @app.command() def start(task_id: str) -> None: """Mark a task as In Progress.""" root = root_path() record_event(root, "command", "rdb start", {"task_id": task_id}) if not update_task_status(root, task_id, "In Progress"): console.print(f"[red]Task not found:[/red] {task_id}") raise typer.Exit(code=1) append_run_log(root, "Task started", task_id=task_id) update_project_state(root, task_id) update_agent_handoff(root, task_id) console.print(f"[green]Started {task_id}.[/green]") @app.command() def complete(task_id: str) -> None: """Mark a task as Done and record validation notes.""" root = root_path() record_event(root, "command", "rdb complete", {"task_id": task_id}) notes = typer.prompt("Validation notes", default="Not tested") if not update_task_status(root, task_id, "Done"): console.print(f"[red]Task not found:[/red] {task_id}") raise typer.Exit(code=1) append_run_log(root, "Task completed", task_id=task_id, notes=notes) update_project_state(root, task_id) update_agent_handoff(root, task_id) console.print(f"[green]Completed {task_id}.[/green]") @app.command() def handoff() -> None: """Print an AI-agent handoff summary.""" root = root_path() record_event(root, "command", "rdb handoff") console.print(build_handoff(root)) @app.command() def prompt() -> None: """Generate a ready-to-paste implementation prompt for the next Todo task.""" root = root_path() record_event(root, "command", "rdb prompt") result = generate_agent_prompt(root) console.print(result) @app.command() def guardrails() -> None: """Review agent runs for signs of non-progress (stalls, repeats, loops).""" root = root_path() record_event(root, "command", "rdb guardrails") results = run_all_guardrails(root) report = format_report(results) console.print(report) @app.command() def generate(min_confidence: str = typer.Option("Medium", help="Minimum confidence to process (High or Medium).")) -> None: """Generate context files from discovery answers using the mapping document.""" root = root_path() record_event(root, "command", "rdb generate") result = generate_context_files(root, min_confidence) if result["generated"]: console.print("[bold green]Generated context content:[/bold green]") for f in result["generated"]: console.print(f"- {f}") else: console.print("[yellow]No context files generated.[/yellow]") if result["skipped"]: console.print("\n[dim]Skipped:[/dim]") for s in result["skipped"]: console.print(f" - {s}")