feat: record CLI command telemetry

This commit is contained in:
2026-06-02 15:49:27 +01:00
parent eb30450f34
commit 2fcc822394
6 changed files with 86 additions and 5 deletions
+3
View File
@@ -0,0 +1,3 @@
{"timestamp": "2026-06-02T14:38:58.532898+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-02T14:43:05.281782+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-02T14:47:33.841698+00:00", "event_type": "command", "target": "rdb status", "details": {}}
+1 -1
View File
@@ -6,7 +6,7 @@ TASKS_READY
## Current Task
TASK-007
TASK-013
## Instructions For Agent
+1 -1
View File
@@ -4,7 +4,7 @@ Current Stage: BUILDING
Previous Stage: BOOTSTRAP_READY
Next Stage: REVIEW_READY
Current Task: TASK-007
Current Task: TASK-013
Active Branch: main
Last Updated: 2026-06-02
+9 -1
View File
@@ -184,7 +184,7 @@ Definition of Done:
## TASK-013 — Record CLI command execution
Status: Todo
Status: Done
Goal:
Record rdb CLI command execution using the telemetry system.
@@ -200,6 +200,14 @@ Acceptance Criteria:
- Include timestamp
- Add/update tests
Definition of Done:
- All 10 CLI commands record a telemetry event on invocation
- Events have event_type "command" and target "rdb <cmd_name>"
- Timestamps are present in UTC ISO format
- New integration tests added to test_telemetry.py (4 new tests)
- All 75 tests pass
## TASK-014 — Integrate telemetry with guardrails
Status: Todo
+15 -2
View File
@@ -13,6 +13,7 @@ from .status import project_stage, task_counts, update_project_state, update_age
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
app = typer.Typer(help="RDB discovery and delivery workflow CLI.")
console = Console()
@@ -35,6 +36,7 @@ def append_run_log(root: Path, event: str, task_id: str = "", notes: str = "") -
def init() -> None:
"""Create the initial markdown project structure."""
root = root_path()
record_event(root, "command", "rdb init")
created: list[str] = []
skipped: list[str] = []
@@ -59,6 +61,7 @@ def init() -> None:
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):
@@ -76,6 +79,7 @@ def discover() -> None:
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:
@@ -119,6 +123,7 @@ def ask_more() -> None:
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)
@@ -138,7 +143,9 @@ def status() -> None:
@app.command(name="next")
def next_task() -> None:
"""Show the next Todo task."""
task = get_next_task(root_path())
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)
@@ -152,6 +159,7 @@ def next_task() -> None:
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)
@@ -165,6 +173,7 @@ def start(task_id: str) -> None:
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}")
@@ -178,13 +187,16 @@ def complete(task_id: str) -> None:
@app.command()
def handoff() -> None:
"""Print an AI-agent handoff summary."""
console.print(build_handoff(root_path()))
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)
@@ -193,6 +205,7 @@ def prompt() -> None:
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)
+57
View File
@@ -127,6 +127,63 @@ def test_read_events_skips_malformed_lines(tmp_path: Path) -> None:
assert events[1].event_type == "also_ok"
# -- CLI command events --
COMMANDS = [
"rdb init",
"rdb discover",
"rdb ask_more",
"rdb status",
"rdb next",
"rdb start",
"rdb complete",
"rdb handoff",
"rdb prompt",
"rdb guardrails",
]
def test_record_event_creates_command_events(tmp_path: Path) -> None:
"""Each CLI command records an event with event_type 'command' and target 'rdb <name>'."""
for target in COMMANDS:
record_event(tmp_path, "command", target)
events = read_events(tmp_path)
assert len(events) == len(COMMANDS)
for event in events:
assert event.event_type == "command"
def test_record_event_target_matches_command_name(tmp_path: Path) -> None:
"""Event target matches 'rdb <cmd>' format."""
for expected in COMMANDS:
record_event(tmp_path, "command", expected)
events = read_events(tmp_path)
targets = [e.target for e in events]
assert set(targets) == set(COMMANDS)
def test_record_command_event_has_timestamp(tmp_path: Path) -> None:
"""Each command event has a non-empty timestamp."""
record_event(tmp_path, "command", "rdb status")
events = read_events(tmp_path)
assert len(events) == 1
assert events[0].timestamp
assert "+" in events[0].timestamp # UTC offset
def test_record_command_event_with_details(tmp_path: Path) -> None:
"""Commands like start/complete can pass details dict."""
record_event(tmp_path, "command", "rdb start", {"task_id": "TASK-013"})
events = read_events(tmp_path)
assert events[0].details["task_id"] == "TASK-013"
record_event(tmp_path, "command", "rdb complete", {"task_id": "TASK-007"})
events = read_events(tmp_path)
assert events[-1].details["task_id"] == "TASK-007"
# -- Helpers --
def log_lines(root: Path) -> list[str]: