12 KiB
12 KiB
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— detectjustrecipes from justfiles.packages/coding-agent/src/tools/recipe/runners/pkg.ts— detectpackage.jsonscripts 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 viatask --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
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
opthrowsToolErrorwith the available task list. - Only the first token is parsed structurally. Everything after the first whitespace run becomes
tailand is appended to the command unchanged. - If
headcontains:and the prefix matches a detected runner id, the suffix must exactly match a task in that runner. - Otherwise
headis 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
<runner-id>:<task>. - 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 sameAgentToolResult<BashToolDetails>shape. - Success path: one text content block containing merged command output (
result.outputfrom bash execution, or(no output)), plus any timeout clamp notice appended after a blank line. - Recipe does not return separate
stdout,stderr, orexitCodefields.stdout/stderrare already merged into the text block by bash execution;exitCodeis only observed indirectly (success requires0, non-zero becomes an error). - Error path: throws
ToolError; for non-zero exits the message is the merged output followed byCommand exited with code <n>. detailsmay 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 anasyncinput.
- 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
RecipeTool.createIf()inpackages/coding-agent/src/tools/recipe/index.tscheckssession.settings.get("recipe.enabled"); disabled returnsnull.- It probes every runner in
RUNNERSfrompackages/coding-agent/src/tools/recipe/runners/index.tswithPromise.all(...)in this order:just,pkg,cargo,make,task. - Each runner returns either
nullor aDetectedRunner { id, label, commandPrefix, tasks }; runners with zero tasks are discarded. - If no runners remain, the tool is not registered.
- Constructor stores detected runners, instantiates
BashTool, renders the model-facing description by passingbuildPromptModel(runners)intopackages/coding-agent/src/prompts/tools/recipe.md, and builds shell renderers fromcreateRecipeToolRenderer(). - On execution,
RecipeTool.execute()callsresolveCommand(op, this.#runners). resolveCommand()inpackages/coding-agent/src/tools/recipe/runner.ts:parseOp()trims only leading whitespace, extracts the first non-whitespace token ashead, and keeps the remainder astail.resolveRunnerAndTask()resolvesheadeither asrunnerId:taskNameor as an unqualified task name.- It throws
ToolErrorfor empty ops, missing explicit tasks, ambiguous task names, or unknown tasks; all error variants include the available task list. - It builds the final shell command with
buildCommand(commandPrefix, commandName, tail), joining non-empty parts with spaces. - If the task defines
cwd, that relative path is returned alongside the command.
RecipeTool.execute()forwards{ command, cwd }intoBashTool.execute(); recipe does not pass timeout, env, async, or pty options.BashTool.execute()resolves internal URLs, validates/normalizes cwd againstsession.cwd, clamps timeout, applies bash interception rules, runs the command, and formats the final result.
Modes / Variants
- Tool enablement:
- Disabled by
recipe.enabledsetting: tool is absent. - Enabled but no detected tasks: tool is absent.
- Disabled by
- Task selection:
- Unqualified task name: succeeds only when exactly one detected runner owns that task.
- Explicit runner-qualified task:
<runner-id>:<task>.
- Runner detection paths:
just: requiresjustonPATH, a justfile, and successfuljust --dump --dump-format=json.pkg: requires a readable rootpackage.json; picks a package manager command from lockfiles orbunavailability; discovers root scripts and workspace package scripts.cargo: requirescargoonPATH,Cargo.toml, and successfulcargo metadata --no-deps --format-version=1.make: requiresmakeonPATHand a makefile; parses targets statically.task: requirestaskonPATH, a Taskfile, and successfultask --list-all --json.
- Execution path:
- Always the synchronous
bashcall surface from recipe inputs. - Bash may still auto-background long-running work if
bash.autoBackground.enabledand session async job support are enabled.
- Always the synchronous
Side Effects
- Filesystem
- Reads manifests from the session cwd during detection: justfiles,
package.json, workspacepackage.jsonfiles,Cargo.toml, makefiles,Taskfile.yml/Taskfile.yaml. - Command execution runs in
session.cwdor a task-specific relative cwd resolved under it. - Bash may allocate output artifacts for truncated command output.
- Reads manifests from the session cwd during detection: justfiles,
- Subprocesses / native bindings
- Detection may spawn
just --dump --dump-format=json,cargo metadata --no-deps --format-version=1, andtask --list-all --json. - Execution spawns the resolved shell command through
BashTool/executeBash().
- Detection may spawn
- 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 = 20per runner inpackages/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 = 300seconds frompackages/coding-agent/src/tools/bash.ts. - Bash clamps timeouts to the configured bash range (
clampTimeout("bash", ...)inpackages/coding-agent/src/tools/bash.ts), but recipe cannot request a custom value. pkgworkspace discovery normalizes workspace globs to.../package.jsonand sorts matched package files lexicographically before task generation.cargodeduplicates generated task names with aSet, 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
nulland log withlogger.debug(...). - Result: the affected runner disappears instead of surfacing an error to the model.
- Missing binaries, missing manifests, parse failures, or non-zero probe exits usually return
- 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.
- Empty
- 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
RecipeToolsetsconcurrency = "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.pkgtask names:- Root
package.jsonscripts keep bare names liketest. - Workspace scripts are always namespaced as
<package-name-or-dir>/<script>and setcwdto that package directory. - Script names are shell-quoted into
commandName, so a task likebuildbecomesbun run 'build'/npm run 'build'/ similar.
- Root
pkgcommand prefix selection prefers lockfiles in this order:bun.lock/bun.lockb,pnpm-lock.yaml,yarn.lock,package-lock.json/npm-shrinkwrap.json; otherwise it falls back tobun runifbunexists, elsenpm run.cargotask names are generated from metadata targets:- Single-package manifests:
bin/<name>,example/<name>,test/<name>. - Multi-package workspaces:
<package>/bin/<name>,<package>/example/<name>,<package>/test/<name>. - Each task overrides
commandPrefixto the fullcargo run ... --bin|--exampleorcargo test ... --testprefix, andcommandNameto the quoted target name.
- Single-package manifests:
maketarget parsing is static text parsing, notmake -qpoutput:- Recognizes makefiles named
Makefile,makefile,GNUmakefile. - Uses
.PHONYlines to decide whether to include undocumented file targets; without any.PHONY, all parsed targets are exposed. - If
.PHONYexists, documented non-phony targets are kept with(file target)appended todoc.
- Recognizes makefiles named
justdetection ignores private recipes and preserves declared parameter names only for prompt display; execution still accepts arbitrarytailtext.taskdetection usesdescfirst, thensummary, for task documentation.- Recipe has no env input of its own. Commands inherit whatever environment
BashToolsupplies for normal bash execution in the session.