feat: record CLI command telemetry
This commit is contained in:
@@ -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
@@ -6,7 +6,7 @@ TASKS_READY
|
|||||||
|
|
||||||
## Current Task
|
## Current Task
|
||||||
|
|
||||||
TASK-007
|
TASK-013
|
||||||
|
|
||||||
## Instructions For Agent
|
## Instructions For Agent
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ Current Stage: BUILDING
|
|||||||
Previous Stage: BOOTSTRAP_READY
|
Previous Stage: BOOTSTRAP_READY
|
||||||
Next Stage: REVIEW_READY
|
Next Stage: REVIEW_READY
|
||||||
|
|
||||||
Current Task: TASK-007
|
Current Task: TASK-013
|
||||||
Active Branch: main
|
Active Branch: main
|
||||||
|
|
||||||
Last Updated: 2026-06-02
|
Last Updated: 2026-06-02
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ Definition of Done:
|
|||||||
|
|
||||||
## TASK-013 — Record CLI command execution
|
## TASK-013 — Record CLI command execution
|
||||||
|
|
||||||
Status: Todo
|
Status: Done
|
||||||
|
|
||||||
Goal:
|
Goal:
|
||||||
Record rdb CLI command execution using the telemetry system.
|
Record rdb CLI command execution using the telemetry system.
|
||||||
@@ -200,6 +200,14 @@ Acceptance Criteria:
|
|||||||
- Include timestamp
|
- Include timestamp
|
||||||
- Add/update tests
|
- 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
|
## TASK-014 — Integrate telemetry with guardrails
|
||||||
|
|
||||||
Status: Todo
|
Status: Todo
|
||||||
|
|||||||
@@ -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 .tasks import generate_agent_prompt, get_next_task, update_task_status
|
||||||
from .templates import CONTEXT_FILES, write_file_if_missing
|
from .templates import CONTEXT_FILES, write_file_if_missing
|
||||||
from .guardrails import run_all_guardrails, format_report
|
from .guardrails import run_all_guardrails, format_report
|
||||||
|
from .telemetry import record_event
|
||||||
|
|
||||||
app = typer.Typer(help="RDB discovery and delivery workflow CLI.")
|
app = typer.Typer(help="RDB discovery and delivery workflow CLI.")
|
||||||
console = Console()
|
console = Console()
|
||||||
@@ -35,6 +36,7 @@ def append_run_log(root: Path, event: str, task_id: str = "", notes: str = "") -
|
|||||||
def init() -> None:
|
def init() -> None:
|
||||||
"""Create the initial markdown project structure."""
|
"""Create the initial markdown project structure."""
|
||||||
root = root_path()
|
root = root_path()
|
||||||
|
record_event(root, "command", "rdb init")
|
||||||
created: list[str] = []
|
created: list[str] = []
|
||||||
skipped: list[str] = []
|
skipped: list[str] = []
|
||||||
|
|
||||||
@@ -59,6 +61,7 @@ def init() -> None:
|
|||||||
def discover() -> None:
|
def discover() -> None:
|
||||||
"""Ask the core discovery questions and append answers to the discovery ledger."""
|
"""Ask the core discovery questions and append answers to the discovery ledger."""
|
||||||
root = root_path()
|
root = root_path()
|
||||||
|
record_event(root, "command", "rdb discover")
|
||||||
write_file_if_missing(root, "context/discovery-log.md", CONTEXT_FILES["context/discovery-log.md"])
|
write_file_if_missing(root, "context/discovery-log.md", CONTEXT_FILES["context/discovery-log.md"])
|
||||||
|
|
||||||
for index, question in enumerate(core_questions(), start=1):
|
for index, question in enumerate(core_questions(), start=1):
|
||||||
@@ -76,6 +79,7 @@ def discover() -> None:
|
|||||||
def ask_more() -> None:
|
def ask_more() -> None:
|
||||||
"""Ask for additional details on Low-confidence or follow-up-needed answers."""
|
"""Ask for additional details on Low-confidence or follow-up-needed answers."""
|
||||||
root = root_path()
|
root = root_path()
|
||||||
|
record_event(root, "command", "rdb ask_more")
|
||||||
answers = read_discovery_answers(root)
|
answers = read_discovery_answers(root)
|
||||||
|
|
||||||
if not answers:
|
if not answers:
|
||||||
@@ -119,6 +123,7 @@ def ask_more() -> None:
|
|||||||
def status() -> None:
|
def status() -> None:
|
||||||
"""Show current project stage and task counts."""
|
"""Show current project stage and task counts."""
|
||||||
root = root_path()
|
root = root_path()
|
||||||
|
record_event(root, "command", "rdb status")
|
||||||
console.print(f"[bold]Project stage:[/bold] {project_stage(root)}")
|
console.print(f"[bold]Project stage:[/bold] {project_stage(root)}")
|
||||||
|
|
||||||
counts = task_counts(root)
|
counts = task_counts(root)
|
||||||
@@ -138,7 +143,9 @@ def status() -> None:
|
|||||||
@app.command(name="next")
|
@app.command(name="next")
|
||||||
def next_task() -> None:
|
def next_task() -> None:
|
||||||
"""Show the next Todo task."""
|
"""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:
|
if not task:
|
||||||
console.print("[yellow]No Todo task found.[/yellow]")
|
console.print("[yellow]No Todo task found.[/yellow]")
|
||||||
raise typer.Exit(code=0)
|
raise typer.Exit(code=0)
|
||||||
@@ -152,6 +159,7 @@ def next_task() -> None:
|
|||||||
def start(task_id: str) -> None:
|
def start(task_id: str) -> None:
|
||||||
"""Mark a task as In Progress."""
|
"""Mark a task as In Progress."""
|
||||||
root = root_path()
|
root = root_path()
|
||||||
|
record_event(root, "command", "rdb start", {"task_id": task_id})
|
||||||
if not update_task_status(root, task_id, "In Progress"):
|
if not update_task_status(root, task_id, "In Progress"):
|
||||||
console.print(f"[red]Task not found:[/red] {task_id}")
|
console.print(f"[red]Task not found:[/red] {task_id}")
|
||||||
raise typer.Exit(code=1)
|
raise typer.Exit(code=1)
|
||||||
@@ -165,6 +173,7 @@ def start(task_id: str) -> None:
|
|||||||
def complete(task_id: str) -> None:
|
def complete(task_id: str) -> None:
|
||||||
"""Mark a task as Done and record validation notes."""
|
"""Mark a task as Done and record validation notes."""
|
||||||
root = root_path()
|
root = root_path()
|
||||||
|
record_event(root, "command", "rdb complete", {"task_id": task_id})
|
||||||
notes = typer.prompt("Validation notes", default="Not tested")
|
notes = typer.prompt("Validation notes", default="Not tested")
|
||||||
if not update_task_status(root, task_id, "Done"):
|
if not update_task_status(root, task_id, "Done"):
|
||||||
console.print(f"[red]Task not found:[/red] {task_id}")
|
console.print(f"[red]Task not found:[/red] {task_id}")
|
||||||
@@ -178,13 +187,16 @@ def complete(task_id: str) -> None:
|
|||||||
@app.command()
|
@app.command()
|
||||||
def handoff() -> None:
|
def handoff() -> None:
|
||||||
"""Print an AI-agent handoff summary."""
|
"""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()
|
@app.command()
|
||||||
def prompt() -> None:
|
def prompt() -> None:
|
||||||
"""Generate a ready-to-paste implementation prompt for the next Todo task."""
|
"""Generate a ready-to-paste implementation prompt for the next Todo task."""
|
||||||
root = root_path()
|
root = root_path()
|
||||||
|
record_event(root, "command", "rdb prompt")
|
||||||
result = generate_agent_prompt(root)
|
result = generate_agent_prompt(root)
|
||||||
console.print(result)
|
console.print(result)
|
||||||
|
|
||||||
@@ -193,6 +205,7 @@ def prompt() -> None:
|
|||||||
def guardrails() -> None:
|
def guardrails() -> None:
|
||||||
"""Review agent runs for signs of non-progress (stalls, repeats, loops)."""
|
"""Review agent runs for signs of non-progress (stalls, repeats, loops)."""
|
||||||
root = root_path()
|
root = root_path()
|
||||||
|
record_event(root, "command", "rdb guardrails")
|
||||||
results = run_all_guardrails(root)
|
results = run_all_guardrails(root)
|
||||||
report = format_report(results)
|
report = format_report(results)
|
||||||
console.print(report)
|
console.print(report)
|
||||||
|
|||||||
@@ -127,6 +127,63 @@ def test_read_events_skips_malformed_lines(tmp_path: Path) -> None:
|
|||||||
assert events[1].event_type == "also_ok"
|
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 --
|
# -- Helpers --
|
||||||
|
|
||||||
def log_lines(root: Path) -> list[str]:
|
def log_lines(root: Path) -> list[str]:
|
||||||
|
|||||||
Reference in New Issue
Block a user