Add permission-aware follow-up flow with interactive resolution

When follow-up agent detects permission-blocked items ("ручное
применение", "permission denied", etc.), they become pending_actions
instead of auto-created tasks. User chooses per item:
  1. Rerun with --dangerously-skip-permissions
  2. Create manual task
  3. Skip

core/followup.py:
  _is_permission_blocked() — regex detection of 9 permission patterns
  generate_followups() returns {created, pending_actions}
  resolve_pending_action() — handles rerun/manual_task/skip

agents/runner.py:
  _run_claude(allow_write=True) adds --dangerously-skip-permissions
  run_agent/run_pipeline pass allow_write through

CLI: kin approve --followup — interactive 1/2/3 prompt per blocked item
API: POST /approve returns {needs_decision, pending_actions}
     POST /resolve resolves individual actions
Frontend: pending actions shown as cards with 3 buttons in approve modal

136 tests, all passing. Frontend builds clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
johnfrum1234 2026-03-15 15:16:48 +02:00
parent 9264415776
commit ab693d3c4d
7 changed files with 356 additions and 73 deletions

View file

@ -203,16 +203,43 @@ def approve_task(task_id: str, body: TaskApprove | None = None):
task_id=task_id,
)
followup_tasks = []
pending_actions = []
if body and body.create_followups:
followup_tasks = generate_followups(conn, task_id)
result = generate_followups(conn, task_id)
followup_tasks = result["created"]
pending_actions = result["pending_actions"]
conn.close()
return {
"status": "done",
"decision": decision,
"followup_tasks": followup_tasks,
"needs_decision": len(pending_actions) > 0,
"pending_actions": pending_actions,
}
class ResolveAction(BaseModel):
action: dict
choice: str # "rerun" | "manual_task" | "skip"
@app.post("/api/tasks/{task_id}/resolve")
def resolve_action(task_id: str, body: ResolveAction):
"""Resolve a pending permission action from follow-up generation."""
from core.followup import resolve_pending_action
if body.choice not in ("rerun", "manual_task", "skip"):
raise HTTPException(400, f"Invalid choice: {body.choice}")
conn = get_conn()
t = models.get_task(conn, task_id)
if not t:
conn.close()
raise HTTPException(404, f"Task '{task_id}' not found")
result = resolve_pending_action(conn, task_id, body.action, body.choice)
conn.close()
return {"choice": body.choice, "result": result}
class TaskReject(BaseModel):
reason: str