1
0

feat: add bug report ingestion pipeline with Forge dispatch (#99)

Replace the stub `handle_bug_report` handler with a real implementation
that logs a decision trail and dispatches code_fix tasks to Forge for
automated fixing. Add `POST /api/bugs/submit` endpoint and `timmy
ingest-report` CLI command so AI test runners (Comet) can submit
structured bug reports without manual copy-paste.

- POST /api/bugs/submit: accepts JSON reports, creates bug_report tasks
- timmy ingest-report: CLI for file/stdin JSON ingestion with --dry-run
- handle_bug_report: logs decision trail to event_log, dispatches
  code_fix task to Forge with parent_task_id linking back to the bug
- 18 TDD tests covering endpoint, handler, and CLI

Co-authored-by: Alexander Payne <apayne@MM.local>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alexander Whitestone
2026-02-28 21:15:53 -05:00
committed by GitHub
parent 2e92838033
commit 6e67c3b421
4 changed files with 577 additions and 7 deletions

View File

@@ -121,5 +121,104 @@ def down():
subprocess.run(["docker", "compose", "down"], check=True)
@app.command(name="ingest-report")
def ingest_report(
file: Optional[str] = typer.Argument(
None, help="Path to JSON report file (reads stdin if omitted)",
),
dry_run: bool = typer.Option(
False, "--dry-run", help="Validate report and show what would be created",
),
):
"""Ingest a structured test report and create bug_report tasks.
Reads a JSON report with an array of bugs and creates one task per bug
in the internal task queue. The task processor will then attempt to
create GitHub Issues for each.
Examples:
timmy ingest-report report.json
timmy ingest-report --dry-run report.json
cat report.json | timmy ingest-report
"""
import json
import sys
# Read input
if file:
try:
with open(file) as f:
raw = f.read()
except FileNotFoundError:
typer.echo(f"File not found: {file}", err=True)
raise typer.Exit(1)
else:
if sys.stdin.isatty():
typer.echo("Reading from stdin (paste JSON, then Ctrl+D)...")
raw = sys.stdin.read()
# Parse JSON
try:
data = json.loads(raw)
except json.JSONDecodeError as exc:
typer.echo(f"Invalid JSON: {exc}", err=True)
raise typer.Exit(1)
reporter = data.get("reporter", "unknown")
bugs = data.get("bugs", [])
if not bugs:
typer.echo("No bugs in report.", err=True)
raise typer.Exit(1)
typer.echo(f"Report: {len(bugs)} bug(s) from {reporter}")
if dry_run:
for bug in bugs:
typer.echo(f" [{bug.get('severity', '?')}] {bug.get('title', '(no title)')}")
typer.echo("(dry run — no tasks created)")
return
# Import and create tasks
from swarm.task_queue.models import create_task
severity_map = {"P0": "urgent", "P1": "high", "P2": "normal"}
created = 0
for bug in bugs:
title = bug.get("title", "")
severity = bug.get("severity", "P2")
description = bug.get("description", "")
if not title or not description:
typer.echo(f" SKIP (missing title or description)")
continue
# Format description with extra fields
parts = [f"**Reporter:** {reporter}", f"**Severity:** {severity}", "", description]
if bug.get("evidence"):
parts += ["", "## Evidence", bug["evidence"]]
if bug.get("root_cause"):
parts += ["", "## Root Cause", bug["root_cause"]]
if bug.get("fix_options"):
parts += ["", "## Fix Options"]
for i, fix in enumerate(bug["fix_options"], 1):
parts.append(f"{i}. {fix}")
task = create_task(
title=f"[{severity}] {title}",
description="\n".join(parts),
task_type="bug_report",
assigned_to="timmy",
created_by=reporter,
priority=severity_map.get(severity, "normal"),
requires_approval=False,
auto_approve=True,
)
typer.echo(f" OK [{severity}] {title}{task.id}")
created += 1
typer.echo(f"\n{created} task(s) created.")
def main():
app()