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:
parent
86e5b8febf
commit
fabae74c19
8 changed files with 1207 additions and 0 deletions
82
cli/main.py
82
cli/main.py
|
|
@ -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
|
||||
# ===========================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue