Add Auto/Review mode toggle and non-interactive runner

- GUI: Auto/Review toggle on TaskDetail and ProjectView
  persisted per-project in localStorage
- Runner: noninteractive param (stdin=DEVNULL, 300s timeout)
  activated by KIN_NONINTERACTIVE=1 env or param
- CLI: --allow-write flag for kin run command
- API: POST /run accepts {allow_write: bool}, sets
  KIN_NONINTERACTIVE=1 and stdin=DEVNULL for subprocess
- Fixes pipeline hanging on interactive claude input (VDOL-002)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Gros Frumos 2026-03-15 17:35:08 +02:00
parent 03961500e6
commit e755a19633
8 changed files with 174 additions and 18 deletions

View file

@ -4,6 +4,7 @@ Each agent = separate process with isolated context.
"""
import json
import os
import sqlite3
import subprocess
import time
@ -24,6 +25,7 @@ def run_agent(
brief_override: str | None = None,
dry_run: bool = False,
allow_write: bool = False,
noninteractive: bool = False,
) -> dict:
"""Run a single Claude Code agent as a subprocess.
@ -64,7 +66,7 @@ def run_agent(
# Run claude subprocess
start = time.monotonic()
result = _run_claude(prompt, model=model, working_dir=working_dir,
allow_write=allow_write)
allow_write=allow_write, noninteractive=noninteractive)
duration = int(time.monotonic() - start)
# Parse output — ensure output_text is always a string for DB storage
@ -109,6 +111,7 @@ def _run_claude(
model: str = "sonnet",
working_dir: str | None = None,
allow_write: bool = False,
noninteractive: bool = False,
) -> dict:
"""Execute claude CLI as subprocess. Returns dict with output, returncode, etc."""
cmd = [
@ -120,13 +123,17 @@ def _run_claude(
if allow_write:
cmd.append("--dangerously-skip-permissions")
is_noninteractive = noninteractive or os.environ.get("KIN_NONINTERACTIVE") == "1"
timeout = 300 if is_noninteractive else 600
try:
proc = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=600, # 10 min max
timeout=timeout,
cwd=working_dir,
stdin=subprocess.DEVNULL if is_noninteractive else None,
)
except FileNotFoundError:
return {
@ -137,7 +144,7 @@ def _run_claude(
except subprocess.TimeoutExpired:
return {
"output": "",
"error": "Agent timed out after 600s",
"error": f"Agent timed out after {timeout}s",
"returncode": 124,
}
@ -213,6 +220,7 @@ def run_pipeline(
steps: list[dict],
dry_run: bool = False,
allow_write: bool = False,
noninteractive: bool = False,
) -> dict:
"""Execute a multi-step pipeline of agents.
@ -260,6 +268,7 @@ def run_pipeline(
brief_override=brief,
dry_run=dry_run,
allow_write=allow_write,
noninteractive=noninteractive,
)
results.append(result)