chore: bootstrap rdb-discovery
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
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, core_questions
|
||||
from .handoff import build_handoff
|
||||
from .status import project_stage, task_counts
|
||||
from .tasks import get_next_task, update_task_status
|
||||
from .templates import CONTEXT_FILES, write_file_if_missing
|
||||
|
||||
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()
|
||||
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()
|
||||
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 status() -> None:
|
||||
"""Show current project stage and task counts."""
|
||||
root = root_path()
|
||||
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."""
|
||||
task = get_next_task(root_path())
|
||||
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()
|
||||
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)
|
||||
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()
|
||||
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)
|
||||
console.print(f"[green]Completed {task_id}.[/green]")
|
||||
|
||||
|
||||
@app.command()
|
||||
def handoff() -> None:
|
||||
"""Print an AI-agent handoff summary."""
|
||||
console.print(build_handoff(root_path()))
|
||||
Reference in New Issue
Block a user