# Headless runs

> kin -p runs one agent turn with no UI at all — the final assistant text goes to stdout, everything else goes to stderr, and the exit code tells a script exactly how the run ended. It's the shape for cron jobs, CI step…

`kin -p` runs one agent turn with no UI at all — the final assistant text goes to stdout, everything else goes to stderr, and the exit code tells a script exactly how the run ended. It's the shape for cron jobs, CI steps, and shell pipelines — and the engine Outpost's [scheduled jobs](/docs/kin/guide/scheduled-jobs/) fire on a timer.

<!-- SOURCE: src/kin/harness/headless.py, src/kin/tui/cli.py, src/kin/harness/session/, src/kin/harness/persistence/, src/kin/harness/tools/sandbox_access.py, src/kin/harness/term_notify.py -->

```bash
kin -p "Read README.md and summarize it in one sentence."
git diff | kin -p "Write a commit message for this diff."
kin -p "Audit the TODOs in src/ and write a report" --output ~/.kin/cron/output/todo-audit/$(date +%s).md
```

## Exit codes — the contract

| Code | Meaning | What a script should do |
|---|---|---|
| `0` | Clean run — the model finished, nothing needed a human. | Consume stdout. |
| `1` | Error — backend failure, context-admission refusal, turn cap, [token budget](#fleet-safety-the-per-run-token-budget), loop guard, or a crashed run. | Check stderr / the report; retry or alert. |
| `2` | **Needs-human** — the run finished (or was cut short) but at least one action was blocked pending human approval or input. | Alert a human; resume the session in the TUI (`kin --resume <id>`, printed on stderr) to answer. |

No mainstream agent CLI distinguishes "blocked pending a human" at the process level — Claude Code aborts a headless run with an undocumented status, and Gemini CLI folds it into a generic tool error. kin reserves `2` for it (promoting Claude Code's *hook-layer* convention, where exit 2 means "blocking — needs attention", to the process level) so automation can tell "failed" from "waiting on you". Argparse usage errors also exit `2` (the argparse default); those happen before any run starts and print a usage message instead of a report.

### Background agents run to quiescence

`kin -p` does not exit while registered background `task` agents are still
working. It processes their lifecycle events through hidden MCA continuations,
waits for active children, and captures final text/report/needs-human state only
after no active agent and no unclaimed event remains. These continuations share
the original human run's token and round limits.

A paused child produces a `quiescence_required` event: the MCA must resume,
interrupt, restart, or kill it. If the MCA leaves it paused or shared limits
expire, Kin closes active children and exits with the normal error contract.
Workflow fan-out remains synchronously owned by the workflow runtime and does
not enter this registry.

Two guardrails keep the quiescence phase honest for automation:

- **A failed run stays failed.** The exit code reflects the *main* turn: a
  supervisor continuation that finishes cleanly after the main turn errored
  (or hit a turn/token cap) cannot flip the run back to exit `0`.
- **A hung agent cannot hang the process.** If an active agent produces no
  lifecycle events *and* no model/tool activity for about five minutes, the
  run ends with exit `1` and an `agent quiescence stalled` error naming the
  stalled agents, instead of waiting forever. A busy long-running agent never
  trips this — any tool or model activity counts as progress.

> **Terminal bell on completion**
>
> If you're running `kin -p` interactively from another shell/tab and watching stderr, a run finishing writes a [terminal bell/OSC 9 notification](/docs/kin/guide/sessions/#terminal-notifications) there (a plain bell everywhere, plus a native desktop notification on a capable terminal — Ghostty, iTerm2, WezTerm, kitty). Only fires when stderr is a real terminal (`sys.stderr.isatty()`) — a redirected/piped run (the normal cron shape) gets no escape bytes in its log. `KIN_NOTIFY=0` disables it.

## The permission envelope — fail-closed, never hanging

Headless runs use the **same modes and the same permission gate** as the TUI — `auto` by default (edits + sandboxed shell run without prompts), `--mode strict` to gate everything. The one thing that changes is what happens when a call *would* prompt:

- In the TUI, an ASK-gated tool call (or the `ask` tool, or `present_plan`) parks the turn on a modal until you answer.
- Headless there is no one to answer, so the call **never runs**. The model instead receives a structured tool error — `error: needs-human: the `write_file` call (…) requires human approval, and this is a headless run with no one to ask…` — so it can adapt, work with the tools that *are* allowed, and finish the turn by reporting what's left for you. The run then exits `2`.

A typed [sandbox-access
request](/docs/kin/guide/auto-mode-and-sandbox/#scoped-sandbox-access) carries one extra
resume guarantee. Kin journals the immutable executor arguments, working
directory, requested capabilities, and pending state before returning
needs-human. If you resume that session in the TUI, the dedicated access modal
reopens before the next model turn; accepting it retries the exact persisted
operation and injects a bounded retry outcome into the resumed turn. The model
never has to reconstruct the command/code from its prose. Decline/cancel also
settles the journal entry.

Nothing is ever silently auto-approved, and nothing hangs: the envelope replaces only the *human resolution* of an ASK — it never widens an ALLOW, and the planning freeze, the protected-write downgrade, and the shell allowlist all apply exactly as in the TUI. See [Modes & permissions](/docs/kin/guide/modes-and-permissions/) and [Auto mode & the sandbox](/docs/kin/guide/auto-mode-and-sandbox/).

> **Strict mode headless = report-only**
>
> `kin -p --mode strict "…"` can read and research but every edit and shell command becomes a needs-human error. Useful when you want a headless run that *proposes* work without doing any of it.

MCP servers are not connected in headless runs (their first-run approval is a human gate), and `--agent` is refused with `-p` (the agent profile's approval/ask modals need a TTY). `--resume` / `--continue` ARE supported headless as of WP2 — see [Resuming headless](#resuming-headless-by-continuation) below.

## Resuming headless by continuation

A needs-human run isn't dead-end headless — you can resume it with a second
`kin -p` whose prompt is the framed human answer:

```bash
# 1. A run that halts on an `ask` (exit 2, sidecar `needs_human`):
kin -p "standardize one env-var name; ask me which" --output /tmp/r.md
# → exit 2; /tmp/r.md.json carries session_id + needs_human[]

# 2. Resume that session with the framed answer as the new prompt:
kin -p --resume <session-id> --output /tmp/r2.md \
  -- "[Operator answer to your pending question(s)]
Q1 (question): KIN_FOO
Continue the task; do not re-ask."
# → exit 0; the model uses the answer and finishes
```

This is **resume by continuation**: the saved journal is full-fidelity and
incremental, so it already contains the model's `ask` tool call **and** the
structured needs-human tool error the headless envelope returned. On resume
the model sees its own question, the "needs-human" error, its own "I'll wait"
finish, then the NEW user turn carrying the framed answer — a natural
continuation. **No history is rewritten** (the alternative — rewriting the
`ask` tool_result in a journal copy — was a planner-reserved fallback that
the live Qwen pass^5 proved unnecessary).

This continuation path remains the right answer for ordinary questions and
approval gates. A pending sandbox-access request is deliberately different:
resume it with the TUI when you want to grant authority and exact-retry the
retained operation. A headless continuation has no modal and therefore cannot
mint that human-owned capability.

Mechanics:

- The resumed run **keeps the same `session_id`** (continuation, not a fork) —
  the sidecar's `session_id` field is the continuation identity, so a
  scheduler can stamp the resumed run row against the original.
- `--continue` resolves to the newest session for the workdir (or exits 1 if
  there isn't one); `--resume <id>` takes an explicit id. A bad id exits 1
  with a clean `[kin] could not resume session …: …` message (NOT a traceback).
- The resumed run can halt again (exit 2) if the model re-asks or hits another
  gate — the needs-human envelope is preserved across resume.
- The Outpost scheduler uses this to answer a halted job via the dashboard
  Inbox (see [Scheduled jobs](/docs/kin/guide/scheduled-jobs/)) — the framed answer is
  built server-side from the inbox item's structured questions.

`--no-save` is ignored on a resume (a resume that doesn't persist is
nonsensical — the journal is the continuation substrate).

## Prompt input

- Positional: `kin -p "do the thing"`.
- Piped stdin: `echo "context" | kin -p` (10 MB cap). `kin -p -` forces stdin.
- Both: piped stdin is prepended as context, the positional is the instruction — `git diff | kin -p "review this diff"`.

## Output

- **stdout** — the final assistant text, nothing else.
- **stderr** — `[kin]`-prefixed diagnostics: errors, the needs-human list, the resume hint, the report path.
- **`--output <file>`** — a markdown run report: status, session id, model, mode, workdir, duration, the prompt, needs-human items (with the resume command), errors, and the final text. Parent directories are created, so a per-job convention like `~/.kin/cron/output/<job>/<timestamp>.md` works out of the box.
- **`<file>.json`** — a machine-readable sidecar written next to every `--output` report: `{version, exit_code, status, done_reason, session_id, model, mode, duration_s, usage: {input, output, total}, needs_human, errors}`. `usage` is the run's cumulative token spend (best-effort — an endpoint that omits usage counts 0, same as the token budget); `needs_human` items carry the structured question payload (`question`/`header`/`options`/`multi_select`) so a scheduler or webhook consumer doesn't parse markdown. The Outpost scheduler reads it to stamp token counts onto run rows and to build the machine door's `job.halted` webhook payload. Best-effort at both ends: a missing/corrupt sidecar degrades to report-only, never fails the run.

## Fleet safety — the per-run token budget

An unattended run on a shared, self-hosted GPU needs a leash: nothing else stops a runaway loop or an over-eager fan-out from burning tokens (and starving your interactive sessions). The **turn cap is unlimited by default** — the doom-loop guard catches identical-repeat stuck loops, but a *progressive* runaway (different calls each round) would otherwise run until the context window fills. The **per-run token budget** is the primary leash:

```bash
kin -p "Audit the TODOs in src/ and write a report" --token-budget 150000
```

- **What counts:** *prompt + completion* tokens, summed across every model round of the run — subagent rounds included (a fan-out draws down the same budget, so it can't multiply the leash). Total (not completion-only) is deliberate: on a self-hosted GPU the whole prompt is re-prefilled every round, so a 40-round loop over a big history is mostly *prompt* compute — a completion-only count would score a runaway near zero.
- **When it stops:** cleanly, at the next model-round boundary — never mid-tool-execution. The round that crosses the line finishes its tool calls (history stays valid, the session resumes normally in the TUI); the *next* model call never fires.
- **How it ends:** done reason `token_budget` → **exit `1`**, with `[kin] the run was cut short: token_budget` on stderr and the reason in the `--output` report. In the TUI the turn ends with a visible system note instead.
- **Where it's set:** `--token-budget N` (headless flag) > `KIN_TOKEN_BUDGET` env > the `token_budget` settings key. `0` / unset = off. A scheduler passes a per-job budget via env — the subprocess picks it up like any other `KIN_*` var.
- **Not model-writable**, on purpose: the budget exists to stop a runaway model, so the model can never raise its own.

A **round cap** is available as a secondary lever for operators who want a hard ceiling independent of tokens: `--max-turns N` / `KIN_MAX_TURNS` / the `max_turns` settings key caps the model↔tool round-trips in one turn (done reason `turn_cap`). `0` = unlimited (the default). For hours-long large-model runs you'll usually leave it off and lean on the token budget + the doom-loop guard.

Before the first model request, countable backends also reserve the configured
response budget inside the context window. Kin may summarize only history that
was complete before the new prompt; the submitted prompt and same-turn
reminders stay verbatim. If those still cannot fit, no model call is made and
the run ends with `done_reason: context_limit`, exit `1`, and a measured
admission error on stderr/report. This keeps an oversized scheduled input from
being mistaken for successful work.

The budget is *best-effort by construction* on endpoints that omit usage from the stream (vLLM and both first-party wires report it). For queue-priority on a shared vLLM (`--scheduling-policy priority`), see the [priority passthrough](/docs/kin/guide/models-and-providers/#priority-scheduling-passthrough-vllm).

## Sessions journal like any other run

A headless run writes the normal session journal (unless `--no-save`), so you
can pick it up interactively afterwards — `kin --resume <session-id>` replays
it in the TUI. Ordinary blocked approvals can be re-driven by asking again; a
pending typed sandbox-access request reopens automatically and resolves before
the next model turn. All the usual configuration applies: `--provider`,
`--model`, `--base-url`, `--preset`, sampling flags, `KIN_*` env vars, and both
`settings.toml` layers resolve exactly as for a [TUI
launch](/docs/kin/getting-started/configuration/). See the [CLI
reference](/docs/kin/reference/cli/) for the full flag list, including the
`-p`-only flags (`--output`, `--token-budget`, `--max-turns`).

## Memory reflection on a clean stop

A run that ends cleanly (`done_reason: stop`) with no pending human gate fires one bounded [memory reflection](/docs/kin/guide/memory/#memory-capture-at-session-end) pass before the session closes — a short extra turn over the live `memory` tool with the full transcript in context, so a `kin -p` run or an Outpost scheduled job writes memory just like an interactive TUI session. It's best-effort and can never change the run's outcome: the pass is bounded to ~90 s, snapshots `done_reason` / `final_text` / `needs_human` *before* it runs, and a run that succeeded is never recorded as `error` because of it. Runs that end any other way (`error`, `context_limit`, `token_budget`, `turn_cap`, `loop_detected`, or `needs_human`) skip it. A pending human gate must keep the append-only journal focused on the blocked task for `--resume`; inserting a memory-curation turn before the answer would make the continuation resume the wrong work. Disable reflection with `memory_reflect = false` / `KIN_MEMORY_REFLECT=0`.

## Cron example

```cron
# Nightly repo digest at 06:30; alert on anything non-zero.
30 6 * * * cd ~/proj && kin -p "Summarize yesterday's git activity" \
  --output ~/.kin/cron/output/digest/$(date +\%Y-\%m-\%d).md \
  || notify-send "kin digest needs attention ($?)"
```
