Add context builder, agent runner, and pipeline executor

core/context_builder.py:
  build_context() — assembles role-specific context from DB.
  PM gets everything; debugger gets gotchas/workarounds; reviewer
  gets conventions only; tester gets minimal context; security
  gets security-category decisions.
  format_prompt() — injects context into role templates.

agents/runner.py:
  run_agent() — launches claude CLI as subprocess with role prompt.
  run_pipeline() — executes multi-step pipelines sequentially,
  chains output between steps, logs to agent_logs, creates/updates
  pipeline records, handles failures gracefully.

agents/specialists.yaml — 8 roles with tools, permissions, context rules.
agents/prompts/pm.md — PM prompt for task decomposition.
agents/prompts/security.md — security audit prompt (OWASP, auth, secrets).

CLI: kin run <task_id> [--dry-run]
  PM decomposes → shows pipeline → executes with confirmation.

31 new tests (15 context_builder, 11 runner, 5 JSON parsing).
92 total, all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
johnfrum1234 2026-03-15 14:03:32 +02:00
parent 86e5b8febf
commit fabae74c19
8 changed files with 1207 additions and 0 deletions

View file

@ -408,6 +408,88 @@ def cost(ctx, period):
click.echo(f"\nTotal: ${total:.4f}")
# ===========================================================================
# run
# ===========================================================================
@cli.command("run")
@click.argument("task_id")
@click.option("--dry-run", is_flag=True, help="Show pipeline plan without executing")
@click.pass_context
def run_task(ctx, task_id, dry_run):
"""Run a task through the agent pipeline.
PM decomposes the task into specialist steps, then the pipeline executes.
With --dry-run, shows the plan without running agents.
"""
from agents.runner import run_agent, run_pipeline
conn = ctx.obj["conn"]
task = models.get_task(conn, task_id)
if not task:
click.echo(f"Task '{task_id}' not found.", err=True)
raise SystemExit(1)
project_id = task["project_id"]
click.echo(f"Task: {task['id']}{task['title']}")
# Step 1: PM decomposes
click.echo("Running PM to decompose task...")
pm_result = run_agent(
conn, "pm", task_id, project_id,
model="sonnet", dry_run=dry_run,
)
if dry_run:
click.echo("\n--- PM Prompt (dry-run) ---")
click.echo(pm_result.get("prompt", "")[:2000])
click.echo("\n(Dry-run: PM would produce a pipeline JSON)")
return
if not pm_result["success"]:
click.echo(f"PM failed: {pm_result.get('output', 'unknown error')}", err=True)
raise SystemExit(1)
# Parse PM output for pipeline
output = pm_result.get("output")
if isinstance(output, str):
try:
output = json.loads(output)
except json.JSONDecodeError:
click.echo(f"PM returned non-JSON output:\n{output[:500]}", err=True)
raise SystemExit(1)
if not isinstance(output, dict) or "pipeline" not in output:
click.echo(f"PM output missing 'pipeline' key:\n{json.dumps(output, indent=2)[:500]}", err=True)
raise SystemExit(1)
pipeline_steps = output["pipeline"]
analysis = output.get("analysis", "")
click.echo(f"\nAnalysis: {analysis}")
click.echo(f"Pipeline ({len(pipeline_steps)} steps):")
for i, step in enumerate(pipeline_steps, 1):
click.echo(f" {i}. {step['role']} ({step.get('model', 'sonnet')}): {step.get('brief', '')}")
if not click.confirm("\nExecute pipeline?"):
click.echo("Aborted.")
return
# Step 2: Execute pipeline
click.echo("\nExecuting pipeline...")
result = run_pipeline(conn, task_id, pipeline_steps)
if result["success"]:
click.echo(f"\nPipeline completed: {result['steps_completed']} steps")
else:
click.echo(f"\nPipeline failed at step: {result.get('error', 'unknown')}", err=True)
if result.get("total_cost_usd"):
click.echo(f"Cost: ${result['total_cost_usd']:.4f}")
if result.get("total_duration_seconds"):
click.echo(f"Duration: {result['total_duration_seconds']}s")
# ===========================================================================
# bootstrap
# ===========================================================================