234 lines
8.1 KiB
Python
234 lines
8.1 KiB
Python
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}")
|