# recipe > Run a task exposed by a detected project task runner. ## Source - Entry: `packages/coding-agent/src/tools/recipe/index.ts` - Model-facing prompt: `packages/coding-agent/src/prompts/tools/recipe.md` - Key collaborators: - `packages/coding-agent/src/tools/recipe/runner.ts` — op parsing, task resolution, prompt model. - `packages/coding-agent/src/tools/recipe/render.ts` — shell-style call/result rendering. - `packages/coding-agent/src/tools/recipe/runners/index.ts` — runner registration order. - `packages/coding-agent/src/tools/recipe/runners/just.ts` — detect `just` recipes from justfiles. - `packages/coding-agent/src/tools/recipe/runners/pkg.ts` — detect `package.json` scripts and workspaces. - `packages/coding-agent/src/tools/recipe/runners/cargo.ts` — detect Cargo run/test targets. - `packages/coding-agent/src/tools/recipe/runners/make.ts` — parse make targets from makefiles. - `packages/coding-agent/src/tools/recipe/runners/task.ts` — detect Taskfile tasks via `task --list-all`. - `packages/coding-agent/src/tools/bash.ts` — actual command execution, truncation, cwd/env handling. ## Inputs | Field | Type | Required | Description | | --- | --- | --- | --- | | `op` | `string` | Yes | Single string containing the task selector plus trailing arguments. The first whitespace-delimited token selects the task; the remainder is appended verbatim to the resolved runner command. Examples from schema/prompt: `test`, `build --release`, `pkg-a/test`, `crate/bin/server`, `pkg:test --watch`. | ### `op` grammar ```text op := S* head (S+ tail)? head := explicit-runner / implicit-task explicit-runner := runner-id ":" task-token implicit-task := task-token runner-id := detected runner id (`just` | `pkg` | `cargo` | `make` | `task`) task-token := first non-whitespace token; may contain `/` tail := remaining characters after the first whitespace run ``` Resolution rules from `resolveRunnerAndTask()`: - Leading whitespace is ignored; an empty `op` throws `ToolError` with the available task list. - Only the first token is parsed structurally. Everything after the first whitespace run becomes `tail` and is appended to the command unchanged. - If `head` contains `:` and the prefix matches a detected runner id, the suffix must exactly match a task in that runner. - Otherwise `head` is treated as a task name and matched across all detected runners. - If exactly one runner has that task, it is used. - If multiple runners have that task, the call is rejected and the error tells the model to use `:`. - Namespaced task names generated by runners use `/`, not `:`. `/` is part of the task name, not a parser separator. ## Outputs - Delegates directly to `BashTool.execute()` and returns the same `AgentToolResult` shape. - Success path: one text content block containing merged command output (`result.output` from bash execution, or `(no output)`), plus any timeout clamp notice appended after a blank line. - Recipe does not return separate `stdout`, `stderr`, or `exitCode` fields. `stdout`/`stderr` are already merged into the text block by bash execution; `exitCode` is only observed indirectly (success requires `0`, non-zero becomes an error). - Error path: throws `ToolError`; for non-zero exits the message is the merged output followed by `Command exited with code `. - `details` may include: - `timeoutSeconds`: effective timeout used by bash. - `requestedTimeoutSeconds`: only when bash clamped a requested timeout; recipe never sets one itself. - `meta`: output truncation metadata from bash execution. - `async`: defined by bash background execution paths, but recipe does not expose an `async` input. - When bash output is truncated, the full text is stored in an artifact and referenced via bash truncation metadata. - Call/result rendering in the TUI uses bash shell rendering with a resolved title, command preview, and optional task cwd. ## Flow 1. `RecipeTool.createIf()` in `packages/coding-agent/src/tools/recipe/index.ts` checks `session.settings.get("recipe.enabled")`; disabled returns `null`. 2. It probes every runner in `RUNNERS` from `packages/coding-agent/src/tools/recipe/runners/index.ts` with `Promise.all(...)` in this order: `just`, `pkg`, `cargo`, `make`, `task`. 3. Each runner returns either `null` or a `DetectedRunner { id, label, commandPrefix, tasks }`; runners with zero tasks are discarded. 4. If no runners remain, the tool is not registered. 5. Constructor stores detected runners, instantiates `BashTool`, renders the model-facing description by passing `buildPromptModel(runners)` into `packages/coding-agent/src/prompts/tools/recipe.md`, and builds shell renderers from `createRecipeToolRenderer()`. 6. On execution, `RecipeTool.execute()` calls `resolveCommand(op, this.#runners)`. 7. `resolveCommand()` in `packages/coding-agent/src/tools/recipe/runner.ts`: 1. `parseOp()` trims only leading whitespace, extracts the first non-whitespace token as `head`, and keeps the remainder as `tail`. 2. `resolveRunnerAndTask()` resolves `head` either as `runnerId:taskName` or as an unqualified task name. 3. It throws `ToolError` for empty ops, missing explicit tasks, ambiguous task names, or unknown tasks; all error variants include the available task list. 4. It builds the final shell command with `buildCommand(commandPrefix, commandName, tail)`, joining non-empty parts with spaces. 5. If the task defines `cwd`, that relative path is returned alongside the command. 8. `RecipeTool.execute()` forwards `{ command, cwd }` into `BashTool.execute()`; recipe does not pass timeout, env, async, or pty options. 9. `BashTool.execute()` resolves internal URLs, validates/normalizes cwd against `session.cwd`, clamps timeout, applies bash interception rules, runs the command, and formats the final result. ## Modes / Variants - Tool enablement: - Disabled by `recipe.enabled` setting: tool is absent. - Enabled but no detected tasks: tool is absent. - Task selection: - Unqualified task name: succeeds only when exactly one detected runner owns that task. - Explicit runner-qualified task: `:`. - Runner detection paths: - `just`: requires `just` on `PATH`, a justfile, and successful `just --dump --dump-format=json`. - `pkg`: requires a readable root `package.json`; picks a package manager command from lockfiles or `bun` availability; discovers root scripts and workspace package scripts. - `cargo`: requires `cargo` on `PATH`, `Cargo.toml`, and successful `cargo metadata --no-deps --format-version=1`. - `make`: requires `make` on `PATH` and a makefile; parses targets statically. - `task`: requires `task` on `PATH`, a Taskfile, and successful `task --list-all --json`. - Execution path: - Always the synchronous `bash` call surface from recipe inputs. - Bash may still auto-background long-running work if `bash.autoBackground.enabled` and session async job support are enabled. ## Side Effects - Filesystem - Reads manifests from the session cwd during detection: justfiles, `package.json`, workspace `package.json` files, `Cargo.toml`, makefiles, `Taskfile.yml` / `Taskfile.yaml`. - Command execution runs in `session.cwd` or a task-specific relative cwd resolved under it. - Bash may allocate output artifacts for truncated command output. - Subprocesses / native bindings - Detection may spawn `just --dump --dump-format=json`, `cargo metadata --no-deps --format-version=1`, and `task --list-all --json`. - Execution spawns the resolved shell command through `BashTool` / `executeBash()`. - Session state (transcript, memory, jobs, checkpoints, registries) - Tool availability depends on session settings. - Constructor prompt text is specialized to detected runners/tasks. - Bash execution may create async job records and output artifacts if bash auto-background triggers. - User-visible prompts / interactive UI - The model-facing tool description lists detected runners and up to 20 tasks per runner. - TUI rendering shows a shell-style preview using the resolved title/command/cwd. - Background work / cancellation - Detection is parallelized across runners. - Runtime command execution honors the passed abort signal through `BashTool`. ## Limits & Caps - Prompt task listing is capped at `PROMPT_TASK_LIMIT = 20` per runner in `packages/coding-agent/src/tools/recipe/runner.ts`; this affects the rendered tool description, not execution. - Recipe itself defines no timeout input; delegated bash execution therefore uses bash's default `timeout = 300` seconds from `packages/coding-agent/src/tools/bash.ts`. - Bash clamps timeouts to the configured bash range (`clampTimeout("bash", ...)` in `packages/coding-agent/src/tools/bash.ts`), but recipe cannot request a custom value. - `pkg` workspace discovery normalizes workspace globs to `.../package.json` and sorts matched package files lexicographically before task generation. - `cargo` deduplicates generated task names with a `Set`, so duplicate targets collapse to one recipe task. ## Errors - Detection failures in runner modules are mostly soft-failed: - Missing binaries, missing manifests, parse failures, or non-zero probe exits usually return `null` and log with `logger.debug(...)`. - Result: the affected runner disappears instead of surfacing an error to the model. - Invocation failures are hard errors from `resolveRunnerAndTask()`: - Empty `op`. - Explicit runner prefix with missing/empty task. - Ambiguous unqualified task name across runners. - Unknown task name. - Execution failures come from `BashTool.execute()`: - Invalid cwd. - Bash interceptor blocks. - Aborts/timeouts. - Non-zero exit codes. - Missing exit status. - All `resolveRunnerAndTask()` errors include the current available task list to help the model retry. ## Notes - `RecipeTool` sets `concurrency = "exclusive"`; calls do not run concurrently with other exclusive tools. - Tool registration is all-or-nothing per runner: a detected runner with zero tasks is dropped. - Runner ids are fixed string literals from the runner modules: `just`, `pkg`, `cargo`, `make`, `task`. - `buildPromptModel()` includes each task's rendered command (`commandPrefix` + `commandName`) and relative cwd when present; the prompt therefore exposes the exact shell form recipe will run. - `pkg` task names: - Root `package.json` scripts keep bare names like `test`. - Workspace scripts are always namespaced as `/