# MCP tools reference

The Roscoe MCP server (started by `roscoe mcp`) exposes 32 tools to Claude
Code over stdio. They're grouped here by purpose. Tool names and input
schemas are the canonical surface: anything not listed here is not part of
the MCP contract.

All tools return JSON wrapped in MCP's `content` envelope. Error shapes are
not uniform across groups:

- Run tools set `isError` only for genuine server failures and return
  `{ error }` for user errors.
- Workflow-writer tools always set `isError: true` and return
  `{ error, code }`, where `code` is a `WriterErrorCode`.
- `test_workflow` returns `{ ok: false, code, message }`.

Treat `code` as the thing to branch on where a tool documents one, and don't
assume `ok: false` is present. The grouping below mirrors
`apps/mcp/src/tools/*.ts`.

## Project scoping (`cwd` / `global`)

Every tool below **except the Docs group** takes a scope input — **exactly one** of:

| Input    | Type    | Notes                                                                                                                                             |
| -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cwd`    | string  | Absolute path to any directory inside the project this call applies to — typically the user's IDE working directory. Resolved to the git root.    |
| `global` | boolean | Set `true` to operate on the global `~/.roscoe` store instead of a project. Use when there is no project directory (e.g. the Claude desktop app). |

The MCP server is long-lived and project-agnostic; the scope tells each call
where it applies. With `cwd`, the server resolves it to its enclosing git
root, derives the project id, and isolates workflows + runs to that project.
Two parallel calls with different `cwd`s see different projects (per-call
AsyncLocalStorage isolation). With `global: true`, there is no project: tools
resolve from the global store, and runs are tracked in a shared global
namespace. Workflow create/move tools may simply OMIT `source` — it defaults
to `repo` when a repo source exists and `global` otherwise — so an explicit
`source: "global"` is optional in global mode, while `source: "repo"` there
returns `no_repo_source`.

Passing **neither** errors with `CWD_REQUIRED`; passing **both** errors with
`CWD_GLOBAL_CONFLICT`. The server never silently guesses a project.

For brevity, the per-tool input tables below **omit the scope row**.
Assume every project-scoped tool requires `cwd` or `global`.

**Validation errors** (returned as structured `{ error, code, cwd? }`):

| Code                     | Meaning                                                                      |
| ------------------------ | ---------------------------------------------------------------------------- |
| `CWD_REQUIRED`           | neither `cwd` nor `global` was passed                                        |
| `CWD_GLOBAL_CONFLICT`    | both `cwd` and `global` were passed                                          |
| `NOT_ABSOLUTE`           | `cwd` was empty or relative                                                  |
| `NOT_FOUND`              | `cwd` does not exist                                                         |
| `NOT_DIR`                | `cwd` is a regular file, not a directory                                     |
| `PROTECTED_PATH`         | `cwd` resolves into a protected system prefix (`/etc`, `/usr`, `/System`, …) |
| `NOT_GIT_REPO`           | `cwd` is not inside a git working tree                                       |
| `PROJECT_ID_UNAVAILABLE` | git remote URL and repo basename both unavailable                            |

**Cross-project access.** Every run-lookup tool — `get_run_status`,
`advance_run`, `advance_consensus`, `advance_round_robin`, `advance_map`,
`resume_run`, `cancel_run`, `get_run_history` — compares the run's
`projectId` to the caller's. A mismatch returns
`"Run not found."` rather than leaking that the run exists in another
project. Scope ownership is strict and symmetric: global-namespace runs
(`projectId = ''`, started with `global: true`) are reachable only by a
global caller, and a project's runs only by that project's caller. This
matches `list_runs`, which filters by the caller's exact project, so the
by-id guard and the listing always agree.

The Docs tools (`list_docs`, `get_doc`, `search_docs`, `ask_docs`,
`answer_docs`) are project-agnostic and do **not** take `cwd`.

## Index

| Group             | Tools                                                                                                                                                                           |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Workflow CRUD     | `list_workflows`, `get_workflow`, `create_workflow`, `update_workflow`, `delete_workflow`, `list_workflow_references`, `move_workflow`, `duplicate_workflow`, `rename_workflow` |
| Authoring helpers | `get_workflow_schema`, `validate_workflow`, `lint_workflow`, `audit_workflow`                                                                                                   |
| Run lifecycle     | `start_workflow`, `get_run_status`, `advance_run`, `advance_consensus`, `advance_round_robin`, `advance_map`, `resume_run`, `cancel_run`                                        |
| Run observability | `list_runs`, `get_run_history`                                                                                                                                                  |
| Verification      | `test_workflow`                                                                                                                                                                 |
| Skill import      | `list_skills`, `convert_skill`, `finalize_skill_conversion`                                                                                                                     |
| Docs              | `list_docs`, `get_doc`, `search_docs`, `ask_docs`, `answer_docs`                                                                                                                |

## Workflow CRUD

### `list_workflows`

Lists all workflows from both Global and Project sources.

| Input | Type | Required |
| ----- | ---- | -------- |
| —     | —    | —        |

**Returns:** `Array<{ id, name, description: string | null, source: 'global' | 'repo', shadowed: boolean }>`

When the same id exists in both sources, repo wins and the global entry has
`shadowed: true`. Cached agent prompts that assume single-dir lookups will
silently start picking up repo workflows once a repo source exists — pass
`source: "global"` to disambiguate.

### `get_workflow`

Returns the full workflow configuration.

| Input         | Type                 | Required |
| ------------- | -------------------- | -------- |
| `workflow_id` | string               | yes      |
| `source`      | `'global' \| 'repo'` | no       |

**Returns:** the parsed `WorkflowConfig` plus a `source` field. When `source` is
omitted, resolves repo-first if a project source exists.

### `create_workflow`

Writes a new workflow YAML file.

| Input       | Type                 | Required | Notes                                                          |
| ----------- | -------------------- | -------- | -------------------------------------------------------------- |
| `id`        | string               | yes      | Must match `[a-zA-Z0-9_-]+`                                    |
| `source`    | `'global' \| 'repo'` | no       | Defaults to `repo` when a project source exists, else `global` |
| `config`    | object               | no       | If omitted, writes a minimal template                          |
| `overwrite` | boolean              | no       | Default false. Refuses on collision when false.                |

**Returns:** `{ id, source, path }` on success, or `{ error, code, details? }`.
Validates schema + integrity before writing. `code` values: `invalid_id`,
`invalid_config`, `integrity_error`, `conflict`, `no_repo_source`,
`io_error`.

### `update_workflow`

Replaces an existing workflow's contents. Validates schema + integrity;
refuses to write on either failure.

| Input    | Type                      | Required |
| -------- | ------------------------- | -------- |
| `id`     | string                    | yes      |
| `source` | `'global' \| 'repo'`      | no       |
| `config` | object (`WorkflowConfig`) | yes      |

**Returns:** `{ ok: true, source, path }` or `{ error, code, details? }`.

### `delete_workflow`

Hard-deletes a workflow file. Requires literal `confirm: true` to actually
delete, and REFUSES when another workflow references this one from a
`subworkflow` node — returning `{ error, referencedBy }` so you can see what
would break. Pass `force: true` to delete anyway.

| Input     | Type                 | Required | Notes                                       |
| --------- | -------------------- | -------- | ------------------------------------------- |
| `id`      | string               | yes      |                                             |
| `source`  | `'global' \| 'repo'` | no       |                                             |
| `confirm` | `true` (literal)     | yes      |                                             |
| `force`   | boolean              | no       | Default `false`. Delete even if referenced. |

**Returns:** `{ ok: true, path, source }`, `{ error, referencedBy }` when
referenced, or `{ error, code }`.

### `list_workflow_references`

List workflows that reference the given workflow through a `subworkflow`
node: the parents that would be orphaned by a delete, or left pointing at a
dead id by a rename without a cascade. Call before `delete_workflow` or
`rename_workflow`. The search covers every workflow on disk regardless of
source, so `source` scopes nothing here; it exists only for input-shape
consistency with the rest of this group.

| Input         | Type                 | Required |
| ------------- | -------------------- | -------- |
| `workflow_id` | string               | yes      |
| `source`      | `'global' \| 'repo'` | no       |

**Returns:** `{ referrers: Array<{ workflowId, nodeId }> }` — one entry per
referencing `subworkflow` node.

### `move_workflow`

Move a workflow between Global and Project. Refuses if the target source
already has the same id.

| Input | Type                 | Required |
| ----- | -------------------- | -------- |
| `id`  | string               | yes      |
| `to`  | `'global' \| 'repo'` | yes      |

**Returns:** `{ ok: true, id, from, to, path }` or `{ error, code }`.

### `duplicate_workflow`

Copy a workflow to a new id (and optionally a new source). Updates the
new file's `name` field to match `to_id`.

| Input         | Type                 | Required |
| ------------- | -------------------- | -------- |
| `from_id`     | string               | yes      |
| `from_source` | `'global' \| 'repo'` | no       |
| `to_id`       | string               | yes      |
| `to_source`   | `'global' \| 'repo'` | yes      |
| `overwrite`   | boolean              | no       |

**Returns:** `{ id, source, path }` or `{ error, code, details? }`.

### `rename_workflow`

Rename the file stem AND update the `name` field on disk. Refuses if
`new_id` already exists in the same source.

**By default the rename cascades**: every workflow whose `subworkflow` node
points at the old id is rewritten to the new one. Pass
`rewrite_references: false` to rename the file alone and leave those
references pointing at an id that no longer resolves.

| Input                | Type                 | Required | Notes                                    |
| -------------------- | -------------------- | -------- | ---------------------------------------- |
| `id`                 | string               | yes      |                                          |
| `new_id`             | string               | yes      |                                          |
| `source`             | `'global' \| 'repo'` | no       |                                          |
| `rewrite_references` | boolean              | no       | Default **`true`** — cascade the rename. |

**Returns:** `{ id, source, path, rewritten, failed, rolledBack }` or
`{ error, code, details? }`. `rewritten` lists the workflows updated,
`failed` those that could not be, and `rolledBack` is true when a partial
failure caused the whole rename to be undone.

## Authoring helpers

### `get_workflow_schema`

Returns the JSON Schema for the workflow YAML, the registered executor
list (with their config keys), the model ids permitted by `roscoe.yaml`, and
an `authoring` array of runtime gotchas worth reading before you write.
Call this once before authoring a new workflow so you have the exact
shapes your write will be validated against.

| Input | Type | Required |
| ----- | ---- | -------- |
| —     | —    | —        |

**Returns:** `{ jsonSchema, executors: ExecutorSummary[], models: { available, default } | { error } }`.

### `validate_workflow`

Dry-run schema + integrity validation. No filesystem writes. Pass either
the full `config` OR `id` (+ optional `source`).

| Input    | Type                 | Required (mutex)        |
| -------- | -------------------- | ----------------------- |
| `config` | object               | one of `config` or `id` |
| `id`     | string               | one of `config` or `id` |
| `source` | `'global' \| 'repo'` | no                      |

**Returns:** `{ valid: true, errors: [] }` or
`{ valid: false, code, errors: string[] }`.

### `lint_workflow`

Schema + integrity validation **plus** a non-fatal lint pass: dead states,
unreachable transitions, models missing from `roscoe.yaml`. Same input
shape as `validate_workflow`.

| Input    | Type                 | Required (mutex)        |
| -------- | -------------------- | ----------------------- |
| `config` | object               | one of `config` or `id` |
| `id`     | string               | one of `config` or `id` |
| `source` | `'global' \| 'repo'` | no                      |

**Returns:** `{ errors: string[], warnings: { code, message, state? }[] }`.
Warning codes include `dead_state`, `unreachable_state`, `unknown_model`.

## Run lifecycle

### `start_workflow`

Start a workflow in MCP mode. Synchronously runs through any
`script` / `condition` nodes, then pauses at the first AI or human
node and returns a `pendingStep`.

| Input                 | Type                                          | Required | Notes                                                                            |
| --------------------- | --------------------------------------------- | -------- | -------------------------------------------------------------------------------- |
| `workflow_id`         | string                                        | yes      |                                                                                  |
| `context`             | record                                        | no       |                                                                                  |
| `source`              | `'global' \| 'repo'`                          | no       |                                                                                  |
| `host`                | `'claude_code' \| 'cowork' \| 'desktop_chat'` | no       | Your surface. Decides whether fan-out can run on the user's subscription.        |
| `spending_cap_usd`    | number (≥ 0)                                  | no       | Run stops with `stop_reason: "spending_cap_exceeded"` once the total exceeds it. |
| `spending_cap_tokens` | integer (≥ 0)                                 | no       | Same, on total input + output tokens.                                            |

**Returns:** `{ runId, status, url, workflow, narration, pendingStep? }`
plus the `AdvanceResult` cost fields, and `hostHint` when a metered
desktop-chat caller hits a fan-out node. `workflow` is `{ id, name,
description }` and `narration` is a ready-to-send opening line naming the
workflow and its live URL. `pendingStep` appears for any node that hands
back — `ai_agent`, `ai_judge`, `consensus`, `human`, `round_robin`, `map`.
`url` points at the run page on the active server (dev port or
`roscoe serve` port) and is a deep link: it carries a `?project=` query
(`?project=global` for a `global: true` run, otherwise the run's
`projectId`) so opening it auto-selects the matching workspace in the web
UI. `runId` is alphanumeric-only so the URL survives markdown rendering in
a chat client unescaped.

### `get_run_status`

Get the current status of a run, including any pending AI step.

| Input    | Type   | Required |
| -------- | ------ | -------- |
| `run_id` | string | yes      |

**Returns:** the same `{ runId, status, url, pendingStep? }` shape.

**Important:** if the returned status is `paused` and the `pendingStep` is
for a `human` node, you must NOT call `resume_run` autonomously — display
the prompt and `validTransitions` to the user as a numbered list and wait
for their reply. Only `ai_agent` / `ai_judge` pending steps may be
answered programmatically (via `advance_run`).

### `advance_run`

Provide Claude's response to a pending AI step. For `ai_judge`, the
response must satisfy the configured validator (the JSON envelope shape
the prompt asked for). For `ai_agent`, any response is accepted.

| Input      | Type   | Required | Notes                                                                                                                                                                                                              |
| ---------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `run_id`   | string | yes      |                                                                                                                                                                                                                    |
| `response` | string | yes      |                                                                                                                                                                                                                    |
| `usage`    | object | no       | Real token counts / cost: `{ inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens, costUsd }`, all optional. Supplying it records the exact API-equivalent cost instead of estimating from text length. |
| `thinking` | string | no       | The model's chain-of-thought for this step. Stored on the turn and shown in the run inspector's Exchange tab.                                                                                                      |

**Returns:** the updated `AdvanceResult` shape (status, optional
`pendingStep` for the next pause).

### `advance_consensus`

Advance a `consensus` step that paused for a subagent-capable host (Claude
Code / Cowork): spawn `agentCount` independent subagents (none sees another's
answer), each on the pendingStep `model` and `prompt` (or, if the pendingStep
carries an `agents` array, subagent `i` runs `agents[i].prompt` on
`agents[i].model`), and return every raw response here.

A most-consistent (USC) node pauses twice, signalled by `consensusPhase`:
`generate` (each subagent returns free-form prose, not a JSON envelope), then
`aggregate` (one subagent picks the most consistent response and returns
`{"reasoning": "...", "index": N}`). For gate/answer nodes Roscoe validates
each verdict; if some fail and a fix could still change the outcome, the node
re-pauses with `consensusPhase: 'retry'` and a reduced `agents[]` covering
only the failed ones.

| Input     | Type                     | Required    | Notes                                                                  |
| --------- | ------------------------ | ----------- | ---------------------------------------------------------------------- |
| `run_id`  | string                   | yes         |                                                                        |
| `results` | `Array<{ raw: string }>` | yes (min 1) | One entry per subagent, in `agents[]` order when that array was given. |

**Returns:** the updated `AdvanceResult` shape.

### `advance_round_robin`

Advance a `round_robin` step, keyed by the pendingStep's `roundRobinPhase`.
When it's `resolve` (candidates need a model call), the pendingStep carries a
single `agents[0].prompt`: run one subagent and return its text as `result`.
Otherwise (`first` / `contested`) the pendingStep carries `criterion`,
`model`, and `matchups` (each `{ id, a, b }`): spawn one independent subagent
per matchup, have it pick A or B (no ties) with a 0-100 decisiveness margin,
and return `verdicts` keyed by matchup `id`. Submit verdicts as waves
complete; the node re-pauses with any still-outstanding matchups (and, in
`contested` mode, a reversed re-judge batch) until every matchup is in.

| Input      | Type                                                                | Required | Notes                                              |
| ---------- | ------------------------------------------------------------------- | -------- | -------------------------------------------------- |
| `run_id`   | string                                                              | yes      |                                                    |
| `verdicts` | `Array<{ id, winner: string, margin: number, reasoning?: string }>` | no       | Matchup rounds only (`first`/`contested`).         |
| `result`   | `{ raw: string }`                                                   | no       | Resolve round only (`roundRobinPhase: 'resolve'`). |

**Returns:** the updated `AdvanceResult` shape.

### `advance_map`

Advance a `map` step. The pendingStep carries `agents` (one `{ model,
prompt }` per subagent), `agentCount`, and `mapPhase`. Run exactly
`agentCount` independent subagents on `agents[i].model` / `agents[i].prompt`
and return each raw response here, in agent order, regardless of phase:
`extract` (one subagent turns a free-text list into branch prompts, then the
node re-pauses with them), `map` (one subagent per branch; an empty response
is a failed branch), or `reduce` (one subagent synthesizes the branch
outputs). After the map round, a configured reduce re-pauses the node once
more if at least one branch succeeded.

| Input     | Type                     | Required    | Notes                                                                                                 |
| --------- | ------------------------ | ----------- | ----------------------------------------------------------------------------------------------------- |
| `run_id`  | string                   | yes         |                                                                                                       |
| `results` | `Array<{ raw: string }>` | yes (min 1) | One entry per subagent, in agent order: `agentCount` on the map round, exactly 1 on the reduce round. |

**Returns:** the updated `AdvanceResult` shape.

### `resume_run`

Resume a workflow paused at a `human` node by supplying the chosen
transition.

| Input        | Type   | Required |
| ------------ | ------ | -------- |
| `run_id`     | string | yes      |
| `transition` | string | yes      |

**Do not call autonomously.** A `human` node represents a required human
decision point. When a run is paused at one, you must (1) display the
node's prompt, (2) present the valid transitions as a numbered list, and
(3) wait for the user to reply with their choice before calling this tool.
Do not infer or assume the choice.

**Returns:** the updated run detail.

### `cancel_run`

Cancel a running or paused workflow. In-flight executor work (a running script
worker or `claude` CLI/SDK call) is aborted so it stops promptly rather than
finishing in the background, and the node the run was sitting on is finalized
with the dedicated `cancelled` status (distinct from `failed`). Errors (not a
no-op) if the run is already terminal.

| Input    | Type   | Required |
| -------- | ------ | -------- |
| `run_id` | string | yes      |

**Returns:** the run detail.

## Run observability

### `list_runs`

List recent workflow runs, newest first.

| Input         | Type                                                     | Required | Notes                                              |
| ------------- | -------------------------------------------------------- | -------- | -------------------------------------------------- |
| `workflow_id` | string                                                   | no       |                                                    |
| `status`      | `pending\|running\|paused\|completed\|failed\|cancelled` | no       |                                                    |
| `since`       | ISO 8601 datetime                                        | no       | Returns runs with `startedAt >= since`             |
| `limit`       | int 1..500                                               | no       | Default 25                                         |
| `kind`        | `'real' \| 'test' \| 'all'`                              | no       | Default `'real'`. `test` shows ephemeral test rows |

**Returns:** an array of `{ id, workflowId, workflowName, status, startedAt,
completedAt, source, workflowSource, kind }`.

### `get_run_history`

Full per-node trace for one run, including child sub-runs (e.g. consensus
agents).

| Input    | Type   | Required |
| -------- | ------ | -------- |
| `run_id` | string | yes      |

**Returns:**

```ts
{
  run: {
    id, workflowId, workflowName, status,
    startedAt, completedAt, source, workflowSource, context,
    parentRunId,        // the parent run, when this is a subworkflow child
    childRunIds,        // subworkflow runs this run spawned
    awaitingChild       // { childRunId } while paused on a subworkflow node
  },
  nodes: Array<{
    id, nodeId, nodeType, status,
    startedAt, completedAt, durationMs, attempts,
    input, output, error,
    children: Array<{ id, nodeId, nodeType, status, ... }>
  }>
}
```

## Verification

### `test_workflow`

Execute a workflow end-to-end against the real runner and the real DB,
then delete the run row in a `finally` block. Identical execution path to
production runs; the only differentiator is `kind: 'test'` on the
`workflow_runs` row, which keeps these out of user-facing run lists.

| Input            | Type                     | Required (mutex)                 | Notes                                  |
| ---------------- | ------------------------ | -------------------------------- | -------------------------------------- |
| `workflow_id`    | string                   | one of `workflow_id` or `config` |                                        |
| `source`         | `'global' \| 'repo'`     | no                               |                                        |
| `config`         | object                   | one of `workflow_id` or `config` | Inline `WorkflowConfig`                |
| `context`        | record                   | no                               | Same shape as `start_workflow.context` |
| `mock_responses` | `Record<nodeId, string>` | no                               | Required for any AI/human node reached |
| `timeout_ms`     | int 100..600000          | no                               | Default 60000                          |

**Returns (success):**

```ts
{
  ok: true,
  finalStatus: 'completed' | 'failed' | 'cancelled',
  durationMs: number,
  nodeTrace: Array<{ nodeId, nodeType, status, input, output, error, children, ... }>
}
```

**Returns (failure):** `{ ok: false, code, message, nodeId?, nodeType?, finalStatus?, durationMs?, nodeTrace?, details? }`.
`code` values: `invalid_input`, `workflow_not_found`, `invalid_config`,
`missing_mock`, `timeout`, `runner_error`,
`fanout_in_test_mode_unsupported`, `unexpected`.

**Limitations and warnings:**

- **Every fan-out node** — `consensus`, `round_robin`, `map`, and
  `subworkflow` (whose child may itself fan out) — calls the live Anthropic
  API directly and cannot be mocked. Workflows containing one are rejected
  with `fanout_in_test_mode_unsupported` unless `ANTHROPIC_API_KEY` is set,
  in which case real API spend will occur. The classification comes from the
  shared `findFanoutNode`, so a new fan-out node type is covered here and in
  `start_workflow`'s gate from one definition — do not re-list the types by
  hand, which is how `round_robin` was once missed.
- The same rejection fires when the workflow config cannot be read.
- `timeout_ms` cannot interrupt a single non-yielding `script` —
  set the script's own timeout for that workload.
- The runner sweeps orphaned `kind: 'test'` rows on MCP startup (gated on
  `ROSCOE_SWEEP_TEST_RUNS=1`, set automatically by the MCP entry point).
- Mocks are required for any `ai_agent`, `ai_judge`, or `human` node the
  run reaches. Missing mocks fail fast with `code: 'missing_mock'` rather
  than hanging.

## Skill import

Convert markdown skills (the Agent Skills open standard — `SKILL.md` files
with `name` + `description` frontmatter, optionally bundled with helper
scripts) into Roscoe workflows. Works across Claude Code, Codex CLI,
OpenCode, Gemini CLI, Cursor, and other harnesses that follow the standard.

### `list_skills`

Find skill candidates by content signature across the standard skill roots
plus the current repo tree. A skill is any `.md` file whose YAML frontmatter
contains both `name:` and `description:` (and is not a Jekyll/Hugo blog
post).

| Input          | Type    | Required | Notes                                                                                                 |
| -------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------- |
| `content_scan` | boolean | no       | Default `true`. Set `false` to check only the standard harness roots and skip the repo-tree fallback. |

Scan roots checked (in order):

- User-global: `~/.claude/skills/`, `~/.agents/skills/`, `~/.codex/skills/`
  (honors `$CODEX_HOME`), `~/.config/opencode/skills/`, `~/.gemini/skills/`
- Project-local (walks up to git root): `.claude/skills/`, `.agents/skills/`,
  `.opencode/skills/`
- Fallback: the repo tree, walked to a depth of 6. It does **not** consult
  `.gitignore` — the only exclusions are a fixed set: `.git`, `node_modules`,
  `dist`, `build`, `.next`, `.turbo`, `.nx`, `coverage`, `.cache`. Skipped
  entirely when `content_scan: false`.

**Returns:** `{ count, skills: Array<{ path, name, description }> }`.

### `convert_skill` + `finalize_skill_conversion`

Convert one skill into a Roscoe workflow, in **two steps** so the conversion
runs on your Claude subscription (the host runs the completion) rather than
the Roscoe server calling a model. Bundled `scripts/` are copied alongside the
workflow and reachable from `script` nodes via `$ROSCOE_WORKFLOW_DIR`. Out of
scope: `references/`, `examples/`, `assets/` folders are detected and reported
but not bundled. For a worked walkthrough, see
[Import a workflow from a skill](/docs/recipes/import-from-skill).

**Step 1 — `convert_skill`** resolves the skill, reads its bundle, and returns
a converter **prompt** for you to run as your own completion. It makes no
server-side model call.

| Input           | Type                 | Required | Notes                                                                                                                              |
| --------------- | -------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `skill_path`    | string               | yes      | Absolute path to a skill directory or markdown file.                                                                               |
| `target_id`     | string               | no       | Workflow id. Defaults to the skill's `name` frontmatter, slug-normalized.                                                          |
| `target_source` | `'global' \| 'repo'` | no       | Defaults to `repo` when a project source exists, otherwise `global`. Pass `'global'` explicitly to force the user-global location. |
| `overwrite`     | boolean              | no       | Default `false`. Refuses on id collision otherwise.                                                                                |

Returns a handback: `{ action: 'run_completion', prompt, model, finalize_args }`.
Run `prompt` yourself, then pass the raw output plus `finalize_args` to
`finalize_skill_conversion`. (Resolve failures return `{ ok: false, stage: 'resolve', message }`.)

**Step 2 — `finalize_skill_conversion`** takes your completion and writes the
workflow. It re-reads the skill bundle from `skill_path`, so nothing you echo
back is trusted as a write source.

| Input              | Type                 | Required | Notes                                                 |
| ------------------ | -------------------- | -------- | ----------------------------------------------------- |
| `raw_model_output` | string               | yes      | The completion you ran on `convert_skill`'s `prompt`. |
| `skill_path`       | string               | yes      | From `finalize_args` (the bundle is re-read from it). |
| `target_id`        | string               | no       | From `finalize_args`.                                 |
| `target_source`    | `'global' \| 'repo'` | yes      | From `finalize_args`.                                 |
| `overwrite`        | boolean              | no       | From `finalize_args`. Default `false`.                |

**Returns:** `{ id, source, path, assetCount, skippedBundleDirs, skippedNote? }`.
Failure shape: `{ ok: false, stage, message }` where `stage` is one of
`resolve`, `parse`, `validate`, `write`. On `parse`/`validate`, the response
includes `rawModelOutput` so you can re-run the prompt or repair — do not
fabricate a workflow to force success.

**Conversion guidance the prompt gives the model:**

- Use `{{ nodeId.field }}` template interpolation whenever a step
  references "the previous result", "the gathered context", etc.
- A reference is only valid if the producing node runs on every path that
  reaches the consuming node (dominance check). Branching workflows must
  reference a node both branches converge through, or use a fallback like
  `{{ nodeId.field || "default" }}`.
- Default to `ai_judge` for routine routing; reach for `consensus` (3-5
  agents with a quorum) for high-stakes decisions (production deploys,
  security severity, migration safety).
- Cycles (`verify → fix → re-verify`) must have `maxIterations` on at
  least one participating node, unless a `human` node sits in the loop.

## Docs

The docs tools cover three retrieval modes plus a synthesized Q&A wrapper.
Pick by intent:

| Tool          | Use when                                                                            |
| ------------- | ----------------------------------------------------------------------------------- |
| `list_docs`   | You need the catalog — slugs and titles for every page.                             |
| `get_doc`     | You already know the slug and want the entire page.                                 |
| `search_docs` | Shallow lookup — ranked snippets are enough.                                        |
| `ask_docs`    | You want full heading sections to ground your own answer (preferred for LLM hosts). |
| `answer_docs` | You want the retrieval packaged into a citation-required prompt to answer yourself. |

### `list_docs`

Returns the flat manifest `[{ slug, title, description }]` for every doc
page on disk.

| Input | Type | Required |
| ----- | ---- | -------- |
| —     | —    | —        |

### `get_doc`

Fetch the full markdown body for one slug.

| Input  | Type   | Required | Notes                                                            |
| ------ | ------ | -------- | ---------------------------------------------------------------- |
| `slug` | string | yes      | No leading slash, no `.md` suffix (e.g. `recipes/approval-flow`) |

**Returns:** `{ frontmatter, markdown }` or `{ error }`.

### `search_docs`

Full-text search across all docs.

| Input   | Type      | Required | Notes                   |
| ------- | --------- | -------- | ----------------------- |
| `query` | string    | yes      | Supports prefix + fuzzy |
| `limit` | int 1..50 | no       | Default 20              |

**Returns:** ranked hits, each with a `headingId` and a snippet of up to
~80 characters either side of the match — call `get_doc` (or
`ask_docs` for richer context) for the full body.

### `ask_docs`

Knowledge-base retrieval. Takes a natural-language question and returns the
most relevant heading **sections** (the markdown from a heading down to the
next equal-or-higher heading), packed within a character budget. Designed for
LLM hosts that want enough grounded context to answer the question themselves.

| Input         | Type           | Required | Notes                            |
| ------------- | -------------- | -------- | -------------------------------- |
| `question`    | string         | yes      | Natural-language question        |
| `maxSections` | int 1..20      | no       | Default 5                        |
| `maxChars`    | int 500..20000 | no       | Total char budget. Default 6000. |

**Returns:**

```ts
{
  question: string;
  sections: Array<{
    slug: string;
    title: string;
    headingId: string | null; // null = page lede
    headingText: string | null;
    text: string; // full section markdown (mermaid + code preserved)
    score: number;
  }>;
  truncated: boolean; // true if more candidates were dropped to fit budget
  totalChars: number;
}
```

The `headingId` matches the id `rehype-slug` produces in the rendered DOM,
so `/docs/${slug}#${headingId}` is a valid deep link.

### `answer_docs`

Q&A packaging. Internally calls `ask_docs`, then returns a citation-required
**prompt** for you to answer yourself — it makes no server-side model call, so
the answer runs on your Claude subscription. Present your answer with the
returned `citations`. LLM hosts that just want the raw source should prefer
`ask_docs`.

| Input         | Type           | Required | Notes                                                          |
| ------------- | -------------- | -------- | -------------------------------------------------------------- |
| `question`    | string         | yes      |                                                                |
| `model`       | string         | no       | Suggested model id for the answer. Default `claude-haiku-4-5`. |
| `maxSections` | int 1..20      | no       | Default 5. Forwarded to `ask_docs`.                            |
| `maxChars`    | int 500..20000 | no       | Default 6000. Forwarded to `ask_docs`.                         |

**Returns:**

```ts
{
  action: 'answer_prompt';
  instructions: string; // answer `prompt` yourself, cite `citations`
  prompt: string; // the citation-required answer prompt to run
  model: string; // suggested model id
  citations: Array<{
    slug: string;
    title: string;
    headingId: string | null;
    headingText: string | null;
  }>;
}
```

**Failure modes:**

- Empty/whitespace question → error.
- Zero relevant sections in retrieval → error (the tool refuses to hand back a
  prompt with no source; fall back to `ask_docs` to inspect the empty retrieval).

## `AdvanceResult` shape

The shared shape returned by `start_workflow`, `get_run_status`,
`advance_run`, `advance_consensus`, `advance_round_robin`, `advance_map`,
`resume_run`, and `cancel_run`:

```ts
{
  runId: string;
  status: 'running' | 'completed' | 'failed' | 'paused' | 'cancelled';
  url: string;          // deep link: /open/runs/<id>?project=<projectId|global>
                        // — /open hands the run to the desktop app when it is
                        //   installed, and falls back to the browser otherwise
  pendingStep?: {
    runId: string;
    nodeId: string;
    nodeType: 'ai_agent' | 'ai_judge' | 'consensus' | 'human' | 'round_robin' | 'map';
    prompt: string;     // full prompt — Claude reads this, calls advance_run
    validTransitions: string[];
    label?: string;               // the node's human-readable name
    transitions?: { event: string; target: string; targetLabel?: string }[];
    agentCount?: number;          // consensus: how many subagents to spawn
    model?: string;               // consensus + round_robin: the model to use
    agents?: { model: string; prompt: string }[];  // per-agent diversity
    consensusPhase?: string;      // generate | aggregate | retry
    criterion?: string;           // round_robin: what to judge on
    matchups?: RoundRobinMatchup[];
    roundRobinPhase?: string;
    mapPhase?: string;
  };
  // plus the cost fields: totalCostUsd, meteredCostUsd,
  // subscriptionEquivCostUsd, billingMode, inputTokens, outputTokens
}
```

## Where to next

- [Using Roscoe over MCP](/docs/running/mcp) — the same surface as a
  walkthrough, including how to connect a host to it.
- [CLI commands](/docs/reference/cli-commands) — `roscoe mcp` itself, and the
  commands that share this server's data.
- [REST API](/docs/reference/rest-api) — the HTTP surface behind the same
  runs and workflows.
- [Workflow sources](/docs/configuration/workflow-sources) — what `source`
  and the global-vs-repo split mean for every tool above.
- [Subscription vs metered](/docs/running/subscription) — what `host` decides,
  and why fan-out nodes hand back instead of calling the API.
