Add backlog audit and task update command

- agents/prompts/backlog_audit.md: QA analyst prompt for checking
  which pending tasks are already implemented in the codebase
- agents/runner.py: run_audit() — project-level agent that reads
  all pending tasks, inspects code, returns classification
- cli/main.py: kin audit <project_id> — runs audit, offers to mark
  done tasks; kin task update <id> --status --priority
- web/api.py: POST /api/projects/{id}/audit (runs audit inline),
  POST /api/projects/{id}/audit/apply (batch mark as done)
- Frontend: "Audit backlog" button on ProjectView with results
  modal showing already_done/still_pending/unclear categories

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Gros Frumos 2026-03-15 17:44:16 +02:00
parent e755a19633
commit 96509dcafc
9 changed files with 548 additions and 2 deletions

View file

@ -220,6 +220,32 @@ def task_show(ctx, id):
click.echo(f" Updated: {t['updated_at']}")
@task.command("update")
@click.argument("task_id")
@click.option("--status", type=click.Choice(
["pending", "in_progress", "review", "done", "blocked", "decomposed"]),
default=None, help="New status")
@click.option("--priority", type=int, default=None, help="New priority (1-10)")
@click.pass_context
def task_update(ctx, task_id, status, priority):
"""Update a task's status or priority."""
conn = ctx.obj["conn"]
t = models.get_task(conn, task_id)
if not t:
click.echo(f"Task '{task_id}' not found.", err=True)
raise SystemExit(1)
fields = {}
if status is not None:
fields["status"] = status
if priority is not None:
fields["priority"] = priority
if not fields:
click.echo("Nothing to update. Use --status or --priority.", err=True)
raise SystemExit(1)
updated = models.update_task(conn, task_id, **fields)
click.echo(f"Updated {updated['id']}: status={updated['status']}, priority={updated['priority']}")
# ===========================================================================
# decision
# ===========================================================================
@ -564,6 +590,65 @@ def run_task(ctx, task_id, dry_run, allow_write):
click.echo(f"Duration: {result['total_duration_seconds']}s")
# ===========================================================================
# audit
# ===========================================================================
@cli.command("audit")
@click.argument("project_id")
@click.pass_context
def audit_backlog(ctx, project_id):
"""Audit pending tasks — check which are already implemented in the code."""
from agents.runner import run_audit
conn = ctx.obj["conn"]
p = models.get_project(conn, project_id)
if not p:
click.echo(f"Project '{project_id}' not found.", err=True)
raise SystemExit(1)
pending = models.list_tasks(conn, project_id=project_id, status="pending")
if not pending:
click.echo("No pending tasks to audit.")
return
click.echo(f"Auditing {len(pending)} pending tasks for {project_id}...")
result = run_audit(conn, project_id)
if not result["success"]:
click.echo(f"Audit failed: {result.get('error', 'unknown')}", err=True)
raise SystemExit(1)
done = result.get("already_done", [])
still = result.get("still_pending", [])
unclear = result.get("unclear", [])
if done:
click.echo(f"\nAlready done ({len(done)}):")
for item in done:
click.echo(f" {item['id']}: {item.get('reason', '')}")
if still:
click.echo(f"\nStill pending ({len(still)}):")
for item in still:
click.echo(f" {item['id']}: {item.get('reason', '')}")
if unclear:
click.echo(f"\nUnclear ({len(unclear)}):")
for item in unclear:
click.echo(f" {item['id']}: {item.get('reason', '')}")
if result.get("cost_usd"):
click.echo(f"\nCost: ${result['cost_usd']:.4f}")
if result.get("duration_seconds"):
click.echo(f"Duration: {result['duration_seconds']}s")
if done and click.confirm(f"\nMark {len(done)} tasks as done?"):
for item in done:
models.update_task(conn, item["id"], status="done")
click.echo(f"Marked {len(done)} tasks as done.")
# ===========================================================================
# bootstrap
# ===========================================================================