Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,7 @@ It provides four generic tools:

- **`start_flow`** — start a new flow from either `definition_id` (the server's flow-definition catalog, e.g. `spec-review`, `code-review`, or a custom definition) **or** `definition_yaml` (an inline dynamic flow definition — the full YAML, same schema as catalog definitions); provide exactly one of the two. Catalog starts use the guarded v2 protocol. A `definition_yaml` flow has extra constraints: every participant must declare `workspace: none` and a concrete, priced model (no `default`), the server clamps `limits`/`mcp` to its own caps rather than trusting the definition, may reject the whole thing with a coded error, and requires a server with dynamic flows enabled. Also provide `target_kind`, `target_ref`, `target_title`, and `context`. Requester context (session ID, cwd, repo root, owner, name) is resolved automatically from the environment. Returns a `flow_run_id`. A single-participant definition starts its first round eagerly; a multi-participant definition starts **round-less** — the response carries no round, and each declared role's agent launches lazily on its first `send_to_participant`. Optional `mode`: by default, when the daemon runs on the same machine and the selected vendor declares a borrowed-context containment strategy, the participant sees current tracked, dirty, and non-ignored untracked checkout content; the safety boundary is vendor-specific. Cursor runs in a daemon-owned snapshot refreshed before follow-up rounds, while direct borrowing is reserved for runtimes with a native read-only tool clamp. The capability is advertised for whatever build of the vendor CLI is installed — it is not gated on a per-version validation record, since a vendor auto-update would then silently drop the participant back onto a stale committed base. It IS gated per platform where the containment boundary has not been measured: **GitHub Copilot advertises borrowed review only on macOS/ARM64, and only on a daemon that can both enforce an OS sandbox (`sandbox-exec`) and broker a token** (`COPILOT_GITHUB_TOKEN`/`GH_TOKEN`/`GITHUB_TOKEN`) — widening its tool surface enough to read a snapshot also widens what a read tool can be pointed at, so the boundary is an OS sandbox rather than the vendor's own permission prompts, and that sandbox grants neither your keychain nor your Copilot state (see [Borrowed-context Copilot reviews](#borrowed-context-copilot-reviews)). Where any of the three is missing, a Copilot borrowed request returns `vendor_containment_unreadable` naming the daemon and the remedies; `mode="context-only"` works and is the remedy. Pass `mode="context-only"` to opt out. Claude review flows currently use owned worktrees because it declares no borrowed-review containment strategy. Optional `vendor`: for reserved `spec-review`/`code-review`, explicitly selects the reviewer independently of the driver; when omitted, resolution falls through to the definition's authored vendor when it declares one, then to your saved `flows.reviewer_vendor` preference (applied via one automatic retry, with the response saying so), and finally a coded `reviewer_vendor_required` if neither is set — ask the user which reviewer vendor to use, pass it explicitly, and offer to save it with `kcap config set flows.reviewer_vendor <vendor>`. Custom single-participant catalog definitions retain their authored vendor unless explicitly overridden. The server records requested/applied vendor plus selection source, and rejects an unavailable or uncertified vendor without silent fallback. Dynamic (`definition_yaml`) flows reject a top-level vendor override because each participant declares its own vendor. Optional `model`: a per-run reviewer **model** override for a single-participant catalog review — REQUIRES `vendor` (the model is interpreted against that vendor; there is no vendor→model table anywhere in the CLI, so `model` without `vendor` is rejected locally before any request is sent) and is rejected on a `definition_yaml` (dynamic) or multi-participant start (each participant already pins its own model). Pass the vendor's own model id or alias verbatim, case-sensitive — the CLI never translates, canonicalizes, or guesses it.
- **`send_to_participant`** — send a follow-up message to a participant role declared by the flow definition (single-participant definitions use `"reviewer"`; the server rejects an unknown role, naming the valid ones). One round runs at a time per role — a second send to a busy role is rejected naming the busy round, while other roles remain addressable. Returns the new round's findings.
- **`get_flow_status`** — get the current status (running, waiting, completed, failed) and last result of a flow run. Optional `wait` (boolean, default `false`): when `true`, blocks — via repeated bounded internal checks, never a raw long-poll — until the round is terminal or roughly 3.5 minutes pass, instead of returning the current snapshot immediately. A long-running round is expected, not an error; on that cap this returns the same benign "still running" text an unset/false `wait` already returns on the round-submission path, so simply call it again with `wait: true`. `flow_run_id` is optional on Claude Code and Codex, the harnesses whose session the flows server can identify (a start from any other harness carries no session, so the id must be passed there): omitted, the newest open flow this session started is read — with several open, the reply lists them to choose from; with none open, the newest settled one, so a failure you were not watching for is still readable — and `session_id` selects another session's flows. The lookup needs a server with the by-session flow route; an older server answers with an error that says to pass the id.
- **`get_flow_status`** — get the current status (running, waiting, completed, failed) and last result of a flow run. Optional `wait` (boolean, default `false`): when `true`, blocks — via repeated bounded internal checks, never a raw long-poll — until the round is terminal or roughly 3.5 minutes pass, instead of returning the current snapshot immediately. A long-running round is expected, not an error; on that cap this returns the same benign "still running" text an unset/false `wait` already returns on the round-submission path, so simply call it again with `wait: true`. `flow_run_id` is optional: omitted, the newest open flow this session started is read (Cursor, Copilot, Gemini, Kiro, OpenCode and Antigravity give the flows server no session, so there it reads the flows started from the same repository — or working directory — on this machine, which the flows server records in `flow-runs-v1.json` under the config directory) — with several open, the reply lists them to choose from; with none open, the newest settled one, so a failure you were not watching for is still readable — and `session_id` selects another session's flows. The lookup needs a server with the by-session flow route; an older server answers with an error that says to pass the id.
- **`close_flow`** — mark a completed flow run as closed.

Every flow response reports which **workspace** the reviewer actually used, so you can tell whether it saw your uncommitted work:
Expand Down Expand Up @@ -616,7 +616,7 @@ The four review tools — **`start_review_flow`**, **`submit_review_round`**, **

#### Harness tool timeouts

A start or round call holds the tool call open while the round runs. The CLI bounds it to end under 5 minutes — the shortest per-tool timeout among the harnesses that drive flows — with the round result or the benign `Flow still running for flow_run_id …` text, which is also the only place the driver is handed its `flow_run_id`. If a harness aborts the call anyway (a shorter timeout in its config, or a user-edited one), the flow is still running server-side: the driver should not start it again and should not go looking for it, but call `get_review_flow_status` / `get_flow_status` with `wait: true`, omitting `flow_run_id` when the aborted start never delivered it (Claude Code and Codex; the other harnesses give the flows server no session to look it up by). The tool descriptions and both flow skills say so up front, because the harness words its own timeout error and kcap cannot.
A start or round call holds the tool call open while the round runs. The CLI bounds it to end under 5 minutes — the shortest per-tool timeout among the harnesses that drive flows — with the round result or the benign `Flow still running for flow_run_id …` text, which is also the only place the driver is handed its `flow_run_id`. If a harness aborts the call anyway (a shorter timeout in its config, or a user-edited one), the flow is still running server-side: the driver should not start it again and should not go looking for it, but call `get_review_flow_status` / `get_flow_status` with `wait: true`, omitting `flow_run_id` when the aborted start never delivered it. The tool descriptions and both flow skills say so up front, because the harness words its own timeout error and kcap cannot.

**Reviewer model override — capability gating, protocol, and semantics:**

Expand Down
6 changes: 3 additions & 3 deletions kcap/skills/agent-flows/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Use the `kcap mcp flows` MCP tools (`start_flow`, `send_to_participant`, `get_fl

`round_timeout` is an **inactivity** bound, not a wall-clock cap: a round only fails for taking too long if the participant goes genuinely quiet for that whole stretch — an actively-working participant can legitimately run a round for a long time. If a status check (or `start_flow`/`send_to_participant`) returns the benign "Flow still running" text, that is an expected outcome on a long round, not a problem — re-enter the wait with `get_flow_status(flow_run_id, wait: true)`, which blocks (via bounded, internally-retried checks — never a raw long-poll) until the round finishes or roughly 3.5 minutes pass, then call it again if it's still running. A round result of **`unclear` now genuinely means the participant went dead or silent** (no activity for the whole inactivity bound, or it crashed/was stopped) — it is no longer a symptom of a merely slow participant; see the `participant_unreachable` and `participant_died`/`participant_stopped` guardrail entries below.

**A harness tool timeout is not a flow failure.** `start_flow` and `send_to_participant` block for minutes while the participant works. If your harness aborts the call with its own tool timeout (on Codex the error reads `timed out awaiting tools/call`), the flow is still running server-side. Do not start a new flow and do not investigate — call `get_flow_status(wait: true)`. Without a `flow_run_id` it reads the newest open flow this session started (pass `session_id` for another session's), so it works even when the aborted start never handed you the id or context compaction dropped it; with several open flows it lists them for you to pick from. That lookup needs a session identity, which the flows server has on Claude Code and Codex only — on any other harness the `flow_run_id` must be passed.
**A harness tool timeout is not a flow failure.** `start_flow` and `send_to_participant` block for minutes while the participant works. If your harness aborts the call with its own tool timeout (on Codex the error reads `timed out awaiting tools/call`), the flow is still running server-side. Do not start a new flow and do not investigate — call `get_flow_status(wait: true)`. Without a `flow_run_id` it reads the newest open flow this session started (pass `session_id` for another session's), so it works even when the aborted start never handed you the id or context compaction dropped it; with several open flows it lists them for you to pick from. On a harness that gives the flows server no session identity, it reads the flows started from this workspace on this machine instead.

## Role-surface safety gate

Expand Down Expand Up @@ -139,7 +139,7 @@ After applying the role-surface safety gate, if `start_flow` / `send_to_particip

## Core rules

1. **Start exactly one flow per user task.** Call `start_flow` once and hold the returned `flow_run_id`. Do NOT start a new flow for follow-up rounds — reuse the same ID. If the id is gone (a harness timeout aborted the start, or compaction dropped it), get it back with `get_flow_status(wait: true)` on Claude Code and Codex — never by starting another flow.
1. **Start exactly one flow per user task.** Call `start_flow` once and hold the returned `flow_run_id`. Do NOT start a new flow for follow-up rounds — reuse the same ID. If the id is gone (a harness timeout aborted the start, or compaction dropped it), get it back with `get_flow_status(wait: true)` — never by starting another flow.
2. **After receiving a non-clean result**, address it, then call `send_to_participant` with the same `flow_run_id` and the updated message.
3. **Do NOT finish the user task while the flow has unresolved results.** Keep iterating until the definition's clean/complete signal.
4. **Only call `close_flow` after the clean signal.** The run stays open until you explicitly close it — don't rely on it closing itself. Then report completion to the user.
Expand Down Expand Up @@ -218,7 +218,7 @@ report completion to user
|---|---|---|---|
| `start_flow` | Exactly one of `definition_id` (catalog id, e.g. `spec-review`, `code-review`, or a custom catalog id) or `definition_yaml` (inline dynamic definition — see "Composing a dynamic flow"); plus `target_kind` (what is being worked on: `spec`, `code`, `pr`, `branch`, `file`, etc.), `target_ref` (a path, branch name, or PR URL/number that identifies the target), `target_title` (short human-readable title), `context` (background context: what to focus on, constraints, definition of done) | `vendor` (reserved aliases only — explicit reviewer vendor; omit to use the definition's authored vendor, or your saved `flows.reviewer_vendor` preference if it declares none), `model` (reserved aliases only — explicit reviewer model override; REQUIRES `vendor`, rejected on dynamic/multi-participant starts), `instructions`, `mode` (`context-only` — optional; by default the participant's worktree is mirrored from THIS SESSION's project directory, not from the directory you are working in. Pass `context-only` to opt out and treat the submitted context as authoritative) | Once, at the start of a flow task. |
| `send_to_participant` | `flow_run_id`, `participant` (role name declared in the flow definition's `participants` map; single-participant definitions use `reviewer` — an unknown role is rejected, naming the valid ones), `message` | `instructions`, `async` (defaults to `true`) | After addressing a non-clean result for that role, or to launch a role for the first time. Pass the same `flow_run_id`, the role's name, and the updated message. |
| `get_flow_status` | — | `flow_run_id` (on Claude Code and Codex, omit to read the newest open flow this session started; several open flows are listed instead — other harnesses must pass it), `session_id` (look up another session's flows; defaults to this session), `wait` (`true`/`false`, defaults to `false`) — when `true`, blocks until the round is terminal or roughly 3.5 minutes pass, instead of returning the current snapshot immediately | Poll or check the current status of a flow run (running, waiting, completed, failed). Use `wait: true` to ride out a long round instead of polling repeatedly yourself, and omit `flow_run_id` to recover a flow whose id you never received or lost. |
| `get_flow_status` | — | `flow_run_id` (omit to read the newest open flow this session started, or on a harness without a session identity the newest one started from this workspace; several open flows are listed instead), `session_id` (look up another session's flows; defaults to this session), `wait` (`true`/`false`, defaults to `false`) — when `true`, blocks until the round is terminal or roughly 3.5 minutes pass, instead of returning the current snapshot immediately | Poll or check the current status of a flow run (running, waiting, completed, failed). Use `wait: true` to ride out a long round instead of polling repeatedly yourself, and omit `flow_run_id` to recover a flow whose id you never received or lost. |
| `close_flow` | `flow_run_id` | — | Only after the definition's clean signal — or when abandoning the task early; the run otherwise stays open until closed. |

## Example (custom definition)
Expand Down
Loading
Loading